<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: chenhao5721865</title>
    <description>The latest articles on DEV Community by chenhao5721865 (@chenhao5721865).</description>
    <link>https://dev.to/chenhao5721865</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172297%2Fe73c3170-7a99-4f28-8e76-28616c3bff32.png</url>
      <title>DEV Community: chenhao5721865</title>
      <link>https://dev.to/chenhao5721865</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chenhao5721865"/>
    <language>en</language>
    <item>
      <title>Validating Factur-X / ZUGFeRD e-invoices in practice: XSD + Schematron</title>
      <dc:creator>chenhao5721865</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:48:02 +0000</pubDate>
      <link>https://dev.to/chenhao5721865/validating-factur-x-zugferd-e-invoices-in-practice-xsd-schematron-4jnp</link>
      <guid>https://dev.to/chenhao5721865/validating-factur-x-zugferd-e-invoices-in-practice-xsd-schematron-4jnp</guid>
      <description>&lt;p&gt;If you build software for European businesses, the clock is ticking: &lt;strong&gt;France makes B2B e-invoicing mandatory from September 2026&lt;/strong&gt; (reception first, then issuance), and &lt;strong&gt;Germany follows in January 2027&lt;/strong&gt;. Both mandates are built on the same foundation — the European standard &lt;strong&gt;EN 16931&lt;/strong&gt; — and both use hybrid formats (Factur-X in France, ZUGFeRD in Germany) that embed a structured XML file inside a human-readable PDF.&lt;/p&gt;

&lt;p&gt;I recently built an e-invoicing API around this, and learned the hard way that "the file is valid XML" is nowhere near enough. This post is what I wish someone had told me before I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Factur-X invoice actually is
&lt;/h2&gt;

&lt;p&gt;A Factur-X file is a regular PDF with an XML attachment (usually &lt;code&gt;factur-x.xml&lt;/code&gt;) embedded via the PDF/A-3 association mechanism. The PDF is for humans; the XML is for machines. The XML follows the &lt;strong&gt;UN/CEFACT CII&lt;/strong&gt; syntax (Cross Industry Invoice) — the other allowed EN 16931 syntax is UBL, used by XRechnung.&lt;/p&gt;

&lt;p&gt;So validating one involves three distinct steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the embedded XML from the PDF&lt;/li&gt;
&lt;li&gt;Check the XML against the XSD (structure, data types)&lt;/li&gt;
&lt;li&gt;Check it against the Schematron business rules (the BR-* rules from EN 16931)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip step 3 and you'll accept invoices that any compliant receiver will reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Extracting the XML with PDFBox 3
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PDDocument&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadPDF&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfBytes&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PDDocumentNameDictionary&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDocumentCatalog&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getNames&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;PDEmbeddedFilesNameTreeNode&lt;/span&gt; &lt;span class="n"&gt;efTree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEmbeddedFiles&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PDComplexFileSpecification&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;efTree&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNames&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Factur-X attaches "factur-x.xml"; ZUGFeRD uses "zugferd-invoice.xml"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entrySet&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PDEmbeddedFile&lt;/span&gt; &lt;span class="n"&gt;ef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getEmbeddedFile&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toByteArray&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not every PDF has attachments.&lt;/strong&gt; A plain PDF invoice (what most French suppliers still send today) will come back with an empty map — that's a &lt;code&gt;422&lt;/code&gt;, not a validation failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the file extension or Content-Type header.&lt;/strong&gt; Check magic bytes (&lt;code&gt;%PDF-&lt;/code&gt; for PDF, &lt;code&gt;&amp;lt;&lt;/code&gt; for raw XML) — clients upload mislabeled files constantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: XSD validation
&lt;/h2&gt;

&lt;p&gt;The EN 16931 CII schema is based on the UN/CEFACT D16B schemas. You load the XSD once and reuse the &lt;code&gt;Schema&lt;/code&gt; (it's thread-safe; &lt;code&gt;Validator&lt;/code&gt; instances are not, so create one per call):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;SchemaFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SchemaFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newInstance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;XMLConstants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;W3C_XML_SCHEMA_NS_URI&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Factur-X_BASICWL.xsd"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newValidator&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setErrorHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// collect, don't throw on first error&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ByteArrayInputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collect all errors instead of failing fast — users fixing invoices want the full list in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Schematron business rules
&lt;/h2&gt;

&lt;p&gt;This is the layer everyone forgets. The XSD checks &lt;em&gt;structure&lt;/em&gt;; Schematron checks &lt;em&gt;meaning&lt;/em&gt;. EN 16931 ships ~200 business rules like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BR-CO-26&lt;/strong&gt;: the seller must have a VAT identifier or a legal registration identifier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BR-07&lt;/strong&gt;: the buyer name must be present&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BR-S-05&lt;/strong&gt;: an invoice line in VAT category "S" (standard rate) must have a VAT rate &amp;gt; 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is my favorite trap: &lt;code&gt;vatRate: 0&lt;/code&gt; with the default category &lt;code&gt;S&lt;/code&gt; is structurally perfect XML — and completely non-compliant. Zero-rated lines must use category &lt;code&gt;Z&lt;/code&gt;. No XSD will ever tell you that.&lt;/p&gt;

&lt;p&gt;In Java, the practical choice is &lt;a href="https://github.com/phax/ph-schematron" rel="noopener noreferrer"&gt;ph-schematron&lt;/a&gt; in "pure" mode (Schematron interpreted directly, no XSLT compilation step):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;SchematronResourcePure&lt;/span&gt; &lt;span class="n"&gt;schematron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SchematronResourcePure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EN16931-CII-validation.sch"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;schematron&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isValidSchematron&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bad rules file"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// validate&lt;/span&gt;
&lt;span class="nc"&gt;SchematronOutputType&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schematron&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applySchematronValidation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ByteArrayInputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat I hit: the &lt;code&gt;applySchematronValidation*&lt;/code&gt; family throws or swallows exceptions depending on which overload you pick — test with malformed input, not just valid samples, or your validator will 500 on exactly the files users most need help with.&lt;/p&gt;

&lt;p&gt;The pure implementation is thread-safe for concurrent validation (I load-tested 8 threads × 25 iterations with no failures), which matters if you're putting this behind an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tooling gap
&lt;/h2&gt;

&lt;p&gt;When I looked for a place to &lt;em&gt;test&lt;/em&gt; all this, the options were surprisingly bad: the official validators are enterprise-priced or require uploading invoices to someone's portal with a signup form. So I built the thing I wanted:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://factur-x-api.com/validateur-factur-x" rel="noopener noreferrer"&gt;Factur-X Validator&lt;/a&gt;&lt;/strong&gt; — free, unlimited, no signup. Drop a Factur-X/ZUGFeRD PDF or raw XML, get the full XSD + Schematron error list with rule IDs (BR-07, etc.) and locations.&lt;/p&gt;

&lt;p&gt;And for anyone who needs to &lt;em&gt;generate&lt;/em&gt; compliant invoices from a SaaS rather than validate them, there's a companion API — &lt;code&gt;POST /api/generate&lt;/code&gt; takes plain JSON (seller, buyer, line items with net prices) and returns an auto-validated EN 16931 XML. All totals (net, VAT, gross) are computed server-side so the output can't be internally inconsistent, and every generated document is round-tripped through the same validator before it's returned. 5 invoices/month free, docs at &lt;a href="https://factur-x-api.com/docs" rel="noopener noreferrer"&gt;factur-x-api.com/docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're preparing for the mandate
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receiving invoices&lt;/strong&gt; (mandatory Sept 2026 in France): you need to &lt;em&gt;read&lt;/em&gt; Factur-X — at minimum, extract the embedded XML reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuing invoices&lt;/strong&gt;: get your totals chain right (net → VAT per rate → gross), pick the right VAT category codes, and validate against Schematron before sending anything. A rejected invoice in the new system isn't a bounced email — it's an unpaid invoice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Curious what others are doing: building EN 16931 support in-house, or going through a PDP (Plateforme de Dématérialisation Partenaire)? And if you try the validator on a real invoice and hit a false positive/negative, I'd genuinely love to hear about it&lt;/p&gt;

</description>
      <category>java</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a free Redis plugin for IntelliJ Community, here's what tripped me up</title>
      <dc:creator>chenhao5721865</dc:creator>
      <pubDate>Fri, 09 Oct 2026 02:13:28 +0000</pubDate>
      <link>https://dev.to/chenhao5721865/i-built-a-free-redis-plugin-for-intellij-community-heres-what-tripped-me-up-4p0o</link>
      <guid>https://dev.to/chenhao5721865/i-built-a-free-redis-plugin-for-intellij-community-heres-what-tripped-me-up-4p0o</guid>
      <description>&lt;p&gt;The official Redis tooling in IntelliJ is Ultimate-only, and I use Community. The third-party plugins on the Marketplace either cost money or fall over in exactly the situations I care about: SSH tunnels, non-UTF-8 data, Remote Development.&lt;/p&gt;

&lt;p&gt;So I wrote my own. It's called Redis Navigator, it's on the JetBrains Marketplace now, and it's completely free. No paid tier, nothing locked.&lt;/p&gt;

&lt;p&gt;Rather than another feature list, I want to write down the bugs that cost me the most time. Maybe it saves someone an evening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bkqqn0pcrf4q0ezfanr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bkqqn0pcrf4q0ezfanr.png" alt=" " width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. localhost killed the SSH tunnel, and the error blamed Redis
&lt;/h2&gt;

&lt;p&gt;Jedis kept throwing:&lt;/p&gt;

&lt;p&gt;Unexpected end of stream&lt;/p&gt;

&lt;p&gt;I spent way too long checking Redis config, firewalls, TLS. All fine. The actual problem: JSch passes the target host string to the SSH &lt;em&gt;server&lt;/em&gt; to resolve. My Redis host was set to &lt;code&gt;localhost&lt;/code&gt;, the Windows OpenSSH server resolved that to IPv6 &lt;code&gt;::1&lt;/code&gt;, and my Redis (Memurai) only listens on IPv4. So the tunnel came up and died immediately, and Jedis reported a dead connection in the least helpful way possible.&lt;/p&gt;

&lt;p&gt;The fix is to resolve the host to a literal IP before forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;literal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;literal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InetAddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getHostAddress&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UnknownHostException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;literal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// jump host with its own internal DNS, pass through as-is&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're debugging tunnel issues: build the channel by hand with &lt;code&gt;ssh -N -L&lt;/code&gt;, push a PING through with a raw socket, and only then test your actual client. Splitting the SSH layer from the Redis layer makes the whole thing obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Editing a String removed its TTL
&lt;/h2&gt;

&lt;p&gt;One of those Redis semantics things: bare &lt;code&gt;SET&lt;/code&gt; clears the TTL. I knew this. I still shipped the bug and only caught it in testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;jedis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SetParams&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setParams&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;keepttl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Pattern search has to drain the SCAN cursor
&lt;/h2&gt;

&lt;p&gt;SCAN is cursor-based and one page is just a partial result. If your pattern search stops after one page, you're silently missing keys. It needs to loop until the cursor wraps back to 0, with some cap in case a pattern goes wild (mine bails at 50k keys). Scroll pagination is only appropriate for plain &lt;code&gt;*&lt;/code&gt; browsing.&lt;/p&gt;

&lt;p&gt;Two Jedis 6 things that got me: &lt;code&gt;Pipeline.sync()&lt;/code&gt; returns void, so batch results need &lt;code&gt;syncAndReturnAll()&lt;/code&gt;. And &lt;code&gt;sscan&lt;/code&gt;/&lt;code&gt;hscan&lt;/code&gt; want the cursor as &lt;code&gt;byte[]&lt;/code&gt; while &lt;code&gt;ScanResult.getCursor()&lt;/code&gt; hands you back a &lt;code&gt;String&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A mojibake bug that was actually a feature request
&lt;/h2&gt;

&lt;p&gt;Testing with Chinese data, everything showed up as garbage. The plugin was fine. The data was fine, technically. I had loaded my test data through a GBK console, so the GBK bytes went straight into Redis and my UTF-8 decoding did exactly what it should: display nonsense.&lt;/p&gt;

&lt;p&gt;Then it clicked: some companies really do have GBK-era data sitting in production Redis. So per-connection charset (UTF-8 / GBK / auto-detect) went from nice-to-have to core feature. GBK data displays correctly and editing in GBK mode round-trips without corrupting anything.&lt;/p&gt;

&lt;p&gt;Practical takeaway: load Chinese test data through a socket or script, never through a console. The other takeaway is to look at your bugs and ask whether they're someone else's daily reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Split Mode testing, blocked by someone else's bug
&lt;/h2&gt;

&lt;p&gt;Remote Development support matters to me, so I tested with &lt;code&gt;runIdeSplitMode&lt;/code&gt; from the IntelliJ Platform Gradle Plugin. The client just hung on "Connecting to remote host..." forever.&lt;/p&gt;

&lt;p&gt;Turned out the plugin had a bug with spaces in the project path: when the backend restarts itself, it goes through a native restarter that drops the Gradle-provided sandbox config, and the new process never starts. My project lives in &lt;code&gt;C:\Project Exploration\...&lt;/code&gt;. Direct hit.&lt;/p&gt;

&lt;p&gt;Workaround was dumb but effective: copy the source to a path without spaces. I filed &lt;a href="https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/2252" rel="noopener noreferrer"&gt;an issue upstream&lt;/a&gt; and JetBrains fixed it within a week, relaunching the backend via exit code instead of the restarter. Once that ships in a release I can delete the workaround.&lt;/p&gt;

&lt;p&gt;The good news is everything in the plugin works in the JetBrains Client: tool window, JSON preview, editing, SSH tunnels, all verified in Split Mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz8lkahjlcthgbc5yzky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz8lkahjlcthgbc5yzky.png" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this sounds useful, the plugin is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plugins.jetbrains.com/plugin/34398-redis-navigator" rel="noopener noreferrer"&gt;Redis Navigator on JetBrains Marketplace&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bugs and feature requests go to &lt;a href="https://github.com/chenhao5721865/redis-navigator-issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt;. It's early days, I read everything and I respond fast.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>java</category>
      <category>intellij</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
