<?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: zbinze</title>
    <description>The latest articles on DEV Community by zbinze (@toolstead).</description>
    <link>https://dev.to/toolstead</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%2F4205621%2F0d3fb5b3-ff3c-4dd0-8687-d04a7becf9d7.png</url>
      <title>DEV Community: zbinze</title>
      <link>https://dev.to/toolstead</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolstead"/>
    <language>en</language>
    <item>
      <title>URL-Safe Base64 vs Standard: How to Tell Them Apart</title>
      <dc:creator>zbinze</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:36:41 +0000</pubDate>
      <link>https://dev.to/toolstead/url-safe-base64-vs-standard-how-to-tell-them-apart-2h6l</link>
      <guid>https://dev.to/toolstead/url-safe-base64-vs-standard-how-to-tell-them-apart-2h6l</guid>
      <description>&lt;p&gt;Standard Base64 uses 64 characters: &lt;code&gt;A&lt;/code&gt;–&lt;code&gt;Z&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;–&lt;code&gt;z&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;9&lt;/code&gt;, and then &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;. Those last two are the problem. In a URL, &lt;code&gt;+&lt;/code&gt; means space in &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;; &lt;code&gt;/&lt;/code&gt; is a path separator. In a filename, &lt;code&gt;/&lt;/code&gt; is a directory separator. So a string that is perfectly valid Base64 becomes structurally dangerous the moment you put it in a query parameter, a cookie value, or a JWT.&lt;/p&gt;

&lt;p&gt;The fix is an alphabet substitution, and it is where a surprising number of decoding failures begin.&lt;/p&gt;

&lt;h1&gt;
  
  
  The substitution, and the name it goes by
&lt;/h1&gt;

&lt;p&gt;URL-safe Base64 — also called base64url, or RFC 4648 §5 — replaces the two troublesome characters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; becomes &lt;code&gt;-&lt;/code&gt; (hyphen)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;_&lt;/code&gt; (underscore)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is identical. Sixty-two characters are shared, so most strings are valid in both alphabets and decode to the same bytes. Only strings containing &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt; tell you which alphabet the producer had in mind.&lt;/p&gt;

&lt;p&gt;Two of those four characters are ambiguous, which is the heart of the problem. If a string contains &lt;code&gt;-&lt;/code&gt;, it is nearly certainly base64url. If it contains &lt;code&gt;+&lt;/code&gt;, it is standard. But a string containing only &lt;code&gt;[A-Za-z0-9]&lt;/code&gt; and &lt;code&gt;=&lt;/code&gt; is valid in both, and there is no way to tell from the text alone. Decoding it either way gives the same answer, so ambiguity is harmless there.&lt;/p&gt;

&lt;p&gt;The dangerous middle case is a &lt;strong&gt;standard&lt;/strong&gt; Base64 string that happens to contain &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt;... except it cannot, because those characters are not in the standard alphabet. Which means the rule is simpler than it looks: if you see &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt;, treat it as base64url and convert; if you see &lt;code&gt;+&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt;, treat it as standard. If you see a mix, the string is not valid in either alphabet and something upstream concatenated two different strings.&lt;/p&gt;

&lt;h1&gt;
  
  
  Padding, again
&lt;/h1&gt;

&lt;p&gt;RFC 4648 §5 allows base64url to omit &lt;code&gt;=&lt;/code&gt; padding, and most producers of JWTs and URL tokens do exactly that. The padding carries no information — it is determined by the length — so it is pure overhead in a token that will be embedded in a header.&lt;/p&gt;

&lt;p&gt;This is why a raw JWT segment often has a length that is not a multiple of four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 36 characters, divisible by 4 by luck. A payload segment is often not. Strict decoders will reject it, so restore padding before decoding — it is deterministic and lossless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;padded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Converting between the two
&lt;/h1&gt;

&lt;p&gt;In JavaScript, browsers, and Node, the conversion is a two-character substitution in each direction, applied to the text form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toUrlSafe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromUrlSafe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node has it built in, in both directions, on both buffers and strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// encode&lt;/span&gt;
&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// decode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python calls it &lt;code&gt;urlsafe_b64decode&lt;/code&gt;, and its counterpart is &lt;code&gt;urlsafe_b64encode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the asymmetry that catches people out: &lt;code&gt;urlsafe_b64decode&lt;/code&gt; accepts both alphabets' characters in some versions, so it will not complain about a standard string containing &lt;code&gt;+&lt;/code&gt;. It is forgiving in the direction where forgiveness is harmless, and strict about padding in the direction where being wrong matters.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why a decoder should report the alphabet it used
&lt;/h1&gt;

&lt;p&gt;If a string contains &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt; and you decode it as standard Base64, you get an error at the exact character that is out of range — which at least tells you something is wrong. But some decoders quietly translate the characters for you, and that is where the worst failure mode lives: a string that is &lt;em&gt;actually standard&lt;/em&gt; Base64 containing a legitimate &lt;code&gt;+&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt; gets mangled into the wrong bytes because the decoder assumed base64url.&lt;/p&gt;

&lt;p&gt;For a picture, that means an image that decodes without error and will not render. For a JWT segment, it means a signature that fails to verify with no explanation.&lt;/p&gt;

&lt;p&gt;That is why the decoder reports which alphabet it chose, and why a checkbox is offered to override it. Our &lt;a href="https://toolstead.net/url-safe-base64-to-image/" rel="noopener noreferrer"&gt;URL-safe Base64 to Image&lt;/a&gt; page exists for the case where you know the source used the URL-safe alphabet and want it treated that way from the start, without guessing. When the input contains &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt; and the switch is off, the tool says so explicitly rather than silently picking one interpretation.&lt;/p&gt;

&lt;h1&gt;
  
  
  The practical rule
&lt;/h1&gt;

&lt;p&gt;Three questions, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Will this string travel through a URL, a filename, or a cookie?&lt;/strong&gt; If yes, produce base64url. It costs nothing and removes an entire class of escaping bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the string contain &lt;code&gt;+&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt;?&lt;/strong&gt; Treat it as standard Base64. Those characters cannot appear in base64url.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it contain &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt;?&lt;/strong&gt; Treat it as base64url.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one habit worth building: never re-encode a string you received before decoding it. Converting a standard string to URL-safe form and back is lossless, but doing it &lt;em&gt;speculatively&lt;/em&gt; — normalising before you know the alphabet — is how a &lt;code&gt;+&lt;/code&gt; becomes a space in a query string and then becomes a different byte after decoding. Decode first, from the form you received, then re-encode in whatever form the next consumer needs.&lt;/p&gt;

&lt;p&gt;The alphabet is only half of what makes a value safe to put in a URL. The other half is the characters around it, where an unescaped &lt;code&gt;&amp;amp;&lt;/code&gt; or &lt;code&gt;#&lt;/code&gt; splits the value with no error at all. &lt;a href="https://toolstead.net/blog/percent-encoding-reserved-characters/" rel="noopener noreferrer"&gt;Percent-encoding: the reserved characters that break URLs&lt;/a&gt; covers that half.&lt;/p&gt;

&lt;h1&gt;
  
  
  Where this shows up in practice
&lt;/h1&gt;

&lt;p&gt;JWTs are the most familiar case: three base64url segments separated by dots, no padding, no line breaks. Their compactness is the whole point, and standard Base64 would break every one of them at the &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Less obvious cases include opaque session cookies, signed URLs (AWS presigned URLs use standard Base64 in some parameters and URL-safe in others), correlation IDs in headers, and image payloads passed as query parameters in APIs that were never designed to carry binary data. In all of them, the failure looks the same: a string that "should decode" produces either an error at one character position, or bytes that decode cleanly and mean nothing.&lt;/p&gt;

&lt;p&gt;The second outcome is the one to watch for. If you decoded something and the result is not the file or the payload you expected, check the alphabet before you check anything else — see &lt;a href="https://toolstead.net/blog/base64-image-broken-fix/" rel="noopener noreferrer"&gt;Base64 image not showing&lt;/a&gt; for the full order of checks, which starts with exactly that question. If you would rather have the alphabet handled for you, the &lt;a href="https://toolstead.net/url-safe-base64-to-image/" rel="noopener noreferrer"&gt;URL-safe Base64 to Image&lt;/a&gt; page treats &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; as the expected alphabet from the start, and &lt;a href="https://toolstead.net/base64-to-image/" rel="noopener noreferrer"&gt;Base64 to Image&lt;/a&gt; auto-detects it while telling you which interpretation it used.&lt;/p&gt;

</description>
      <category>base64</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Card Number Formats Differ: Hex, Decimal, Byte Order</title>
      <dc:creator>zbinze</dc:creator>
      <pubDate>Wed, 28 Oct 2026 01:35:13 +0000</pubDate>
      <link>https://dev.to/toolstead/why-card-number-formats-differ-hex-decimal-byte-order-3fp5</link>
      <guid>https://dev.to/toolstead/why-card-number-formats-differ-hex-decimal-byte-order-3fp5</guid>
      <description>&lt;p&gt;A card holds a few bytes of opaque identifier. The number a reader shows you is a &lt;strong&gt;projection&lt;/strong&gt; of those bytes — a base and a byte order chosen by whoever wrote the firmware — and nothing more. That is why the same credential can read as &lt;code&gt;04A2243A&lt;/code&gt; on the installer's laptop, &lt;code&gt;04:A2:24:3A&lt;/code&gt; on the controller, &lt;code&gt;77734970&lt;/code&gt; in one database and &lt;code&gt;975479300&lt;/code&gt; in another. Four correct answers, zero agreement.&lt;/p&gt;

&lt;p&gt;Once the formats are seen as &lt;em&gt;bytes plus two decisions&lt;/em&gt; — which base, and which end of the byte string is byte zero — the conversions stop looking like magic and start looking like a checklist.&lt;/p&gt;

&lt;h1&gt;
  
  
  A card UID is bytes; the number is a projection
&lt;/h1&gt;

&lt;p&gt;125 kHz proximity cards and Mifare Classic carry a &lt;strong&gt;4-byte&lt;/strong&gt; UID. DESFire, Ultralight and many ISO 14443-4 cards carry &lt;strong&gt;7 bytes&lt;/strong&gt;; a handful carry 10. Those bytes are an identifier, not a quantity: there is no "true" decimal value for a UID, only the value some system computed from it.&lt;/p&gt;

&lt;p&gt;The length of the number is therefore evidence. A decimal number above &lt;code&gt;4,294,967,295&lt;/code&gt; (2^32 − 1) cannot have come from 4 bytes. A number above roughly &lt;code&gt;72,057,594,037,927,935&lt;/code&gt; (2^56 − 1) cannot have come from 7 bytes. Those two bounds usually narrow the card family down before anything else has been looked at — and they are the reason a "16-digit decimal card number" is a contradiction unless the producer is printing something other than a raw UID, such as a printed credential number from a separate database column.&lt;/p&gt;

&lt;h1&gt;
  
  
  The forms, and how they relate
&lt;/h1&gt;

&lt;p&gt;Take one card whose UID is &lt;code&gt;04 A2 24 3A&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes on the card      04 A2 24 3A
big-endian hex         04A2243A            ← the standard hex print
colon-grouped hex      04:A2:24:3A         ← the same bytes, readable
decimal (big-endian)   77,734,970          ← 04A2243A as a 32-bit integer
little-endian hex      3A24A204            ← the byte string reversed
reversed decimal       975,479,300         ← the reversal, as an integer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that list again, because it contains the whole problem: &lt;code&gt;77,734,970&lt;/code&gt; and &lt;code&gt;975,479,300&lt;/code&gt; are the &lt;em&gt;same card&lt;/em&gt;. A system given the wrong one will accept nothing at all, or worse, accept a number that belongs to a different credential.&lt;/p&gt;

&lt;p&gt;The two directions that appear in the wild are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Big-endian (network order).&lt;/strong&gt; The first byte is the most significant. This is what hex printing produces, what a &lt;code&gt;printf&lt;/code&gt; loop over bytes produces, and what most access-control software stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Little-endian, reversed.&lt;/strong&gt; The byte string is flipped before it becomes a number. This is what a large share of 125 kHz readers print, especially in Asia, which is why the number on the card holder's paperwork rarely matches the number in the controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mapping is a &lt;strong&gt;byte&lt;/strong&gt; reversal, not a digit reversal. Reversing &lt;code&gt;975,479,300&lt;/code&gt; as digits gives &lt;code&gt;003,974,579&lt;/code&gt;, which is not a card at all; reversing the bytes gives &lt;code&gt;3A24A204&lt;/code&gt;, and reversing that gives back &lt;code&gt;04A2243A&lt;/code&gt;. A converter that reverses decimal digits has misunderstood the format, and the numbers it produces look entirely plausible.&lt;/p&gt;

&lt;h1&gt;
  
  
  Where the conversion invents a number
&lt;/h1&gt;

&lt;p&gt;Three failure modes account for most "this card works on one door and not the other" reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thousands separators.&lt;/strong&gt; Copy a column out of Excel and you get the &lt;em&gt;display text&lt;/em&gt;: &lt;code&gt;2,864,434,397&lt;/code&gt;, commas included. Split naively on commas and one card becomes two cells, &lt;code&gt;2&lt;/code&gt; and &lt;code&gt;864,434,397&lt;/code&gt; — and a converter that reports success has just produced two card numbers that do not exist. The correct reading is that &lt;code&gt;2,864,434,397&lt;/code&gt; is one value (it is &lt;code&gt;AABBCCDD&lt;/code&gt; in hex), and the correct behaviour for the ambiguous case is to refuse the input rather than guess. The judgement call is whether a comma is a thousands separator or a column delimiter; it needs a test, not a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leading zeros.&lt;/strong&gt; A byte of &lt;code&gt;0x0A&lt;/code&gt; is &lt;code&gt;10&lt;/code&gt; in decimal and &lt;code&gt;0A&lt;/code&gt; in hex — the leading zero survives in hex and disappears in decimal. A 4-byte UID whose first byte is &lt;code&gt;0x00&lt;/code&gt; is legal, and it becomes indistinguishable from a 3-byte number the moment it is stored as an integer. Store UIDs as byte strings or as fixed-width hex, never as integers, or the first byte you lose will be the one that made the card unique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precision.&lt;/strong&gt; A 7-byte UID does not always fit in a JavaScript &lt;code&gt;Number&lt;/code&gt;. &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; is &lt;code&gt;9,007,199,254,740,992&lt;/code&gt; (2^53), and any 7-byte UID whose first byte is &lt;code&gt;0x20&lt;/code&gt; or above exceeds it. NXP-style UIDs that begin with &lt;code&gt;0x04&lt;/code&gt; top out at &lt;code&gt;1,407,374,883,553,279&lt;/code&gt; and stay inside the safe range by luck — a property of that one leading byte, not something to rely on. Parse the hex digits with &lt;code&gt;BigInt&lt;/code&gt; when the field is 7 bytes wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;461B6C44F53981&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// 19733400197085570  ← wrong&lt;/span&gt;
&lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// 19733400197085569  ← the card&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two values differ in the last digit, and the wrong one is not an error: it is a valid number that will enroll happily and never match the card again. That is the characteristic failure of this whole subject — a plausible wrong answer instead of a failure.&lt;/p&gt;

&lt;h1&gt;
  
  
  Parsing a column instead of a value
&lt;/h1&gt;

&lt;p&gt;Real work arrives as a column pasted out of a spreadsheet, not as one clean string, and the column is rarely uniform: some rows decimal as the reader printed them, some hex with colons, some reversed. Guessing a format per cell is how half a table ends up wrong while the tool reports no failures at all.&lt;/p&gt;

&lt;p&gt;What is needed is a converter that says which row it rejected and why. "Row 14: five colon-separated groups is not a 4-byte or 7-byte UID" is actionable; "invalid input" for 200 rows is not. The &lt;a href="https://toolstead.net/card-number-converter/" rel="noopener noreferrer"&gt;card number converter&lt;/a&gt; works that way by design: paste a column in any of the forms above and it shows every form of each value, refuses rows it cannot read unambiguously, and exports the table as CSV for whatever system needs it next. It reads the bytes and prints projections — it never rounds a projection back into bytes and calls the result the card.&lt;/p&gt;

&lt;h1&gt;
  
  
  When two systems disagree, check in this order
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Length.&lt;/strong&gt; 4 bytes, 7 bytes or 10? Half the hypotheses are eliminated by this alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte order.&lt;/strong&gt; Reverse the bytes and compare again. If the reversed value matches, the disagreement is found and it is not a hardware fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leading zeros.&lt;/strong&gt; Compare the hex forms rather than the decimal ones; if one side is a byte short, that is the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base.&lt;/strong&gt; &lt;code&gt;10000000&lt;/code&gt; could be decimal, or hex, or a number that happens to look round. Compare as fixed-width hex before concluding that two systems hold different cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame, not UID.&lt;/strong&gt; If the number came out of a Wiegand frame rather than being a raw UID, the split between facility code and card number is involved — see &lt;a href="https://toolstead.net/blog/wiegand-26-vs-34/" rel="noopener noreferrer"&gt;Wiegand 26 vs 34&lt;/a&gt;, where one credential legitimately becomes two different numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While debugging, keep hex and Base64 apart. Hex is one character per nibble and is a &lt;em&gt;representation&lt;/em&gt; of the bytes; Base64 is a different encoding with a four-for-three expansion, and a string that looks hexadecimal is not automatically hex — &lt;a href="https://toolstead.net/blog/base64-vs-hex-base32-ascii85/" rel="noopener noreferrer"&gt;the comparison of hex, Base32, Base64 and Ascii85&lt;/a&gt; covers how to tell them apart from the string itself.&lt;/p&gt;

&lt;p&gt;The habit that prevents most of this: normalise on the way in, keep the bytes, and print only the form the receiving system asked for. Every extra round trip through a decimal integer is another chance to reverse something that was already reversed.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>security</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Percent-Encoding: The Reserved Characters That Break URLs</title>
      <dc:creator>zbinze</dc:creator>
      <pubDate>Tue, 27 Oct 2026 01:43:42 +0000</pubDate>
      <link>https://dev.to/toolstead/percent-encoding-the-reserved-characters-that-break-urls-foi</link>
      <guid>https://dev.to/toolstead/percent-encoding-the-reserved-characters-that-break-urls-foi</guid>
      <description>&lt;p&gt;A URL is not a string with escapes in it. It is a small grammar with several interchangeable parts — scheme, authority, path, query, fragment — and a character's meaning depends on which part it appears in. Percent-encoding exists so that data can be carried inside those parts without being mistaken for the grammar itself. When that goes wrong, nothing reports an error: the value simply arrives shorter, split in two, or pointing somewhere the server never sees.&lt;/p&gt;

&lt;h1&gt;
  
  
  The two sets, and one detail about them
&lt;/h1&gt;

&lt;p&gt;RFC 3986 divides characters into two groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unreserved&lt;/strong&gt; — &lt;code&gt;A&lt;/code&gt;–&lt;code&gt;Z&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;–&lt;code&gt;z&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;9&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;. These never need encoding, and encoding them anyway creates a second spelling of the same URL. That looks harmless until two spellings reach a cache, a signature calculation or a URL-based access rule, and are treated as two different resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserved&lt;/strong&gt;, in two flavours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gen-delims&lt;/strong&gt; — &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sub-delims&lt;/strong&gt; — &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reserved does not mean "always encode". It means "this character has a job here". &lt;code&gt;:&lt;/code&gt; is a delimiter in the authority (&lt;code&gt;host:port&lt;/code&gt;) and perfectly ordinary data in a path segment. &lt;code&gt;&amp;amp;&lt;/code&gt; is a separator between query parameters and perfectly ordinary data inside a path. The rule is always relative to the component you are writing.&lt;/p&gt;

&lt;p&gt;One detail that trips parsers: percent-encoded triplets are &lt;strong&gt;case-insensitive&lt;/strong&gt; — &lt;code&gt;%2F&lt;/code&gt; and &lt;code&gt;%2f&lt;/code&gt; mean the same octet — but string equality in your code is not. Canonicalise to uppercase hex before comparing, hashing or signing, or two identical URLs will produce two different signatures.&lt;/p&gt;

&lt;h1&gt;
  
  
  The characters that cause incidents
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; in a query value.&lt;/strong&gt; The value ends at the &lt;code&gt;&amp;amp;&lt;/code&gt;; everything after it becomes another parameter. Symptom: a long token arrives truncated at an arbitrary point, and an extra parameter appears in the request that no client intended to send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#&lt;/code&gt; anywhere in a query value.&lt;/strong&gt; Everything from the &lt;code&gt;#&lt;/code&gt; onward becomes the fragment, and a browser never sends the fragment to the server. So the server sees the value cut at that point, and the browser's own address bar shows the rest — the single most confusing version of this bug, because the URL looks complete to everyone who inspects it locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;+&lt;/code&gt; in a query value.&lt;/strong&gt; Under &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; rules it means a space, so &lt;code&gt;C++&lt;/code&gt; becomes &lt;code&gt;C&lt;/code&gt;. In a path it is a literal plus. Send a literal plus as &lt;code&gt;%2B&lt;/code&gt; if you want it to survive being handled by two different libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;%&lt;/code&gt; on its own.&lt;/strong&gt; A literal percent sign must be &lt;code&gt;%25&lt;/code&gt;. A single &lt;code&gt;%&lt;/code&gt; followed by something that is not two hex digits makes the URL invalid, and strict parsers reject the whole thing rather than the character — which is why an input like &lt;code&gt;100%&lt;/code&gt; or &lt;code&gt;%AB&lt;/code&gt; in a template can break an otherwise fine request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Space.&lt;/strong&gt; &lt;code&gt;%20&lt;/code&gt; in a path and in a standards-compliant query; &lt;code&gt;+&lt;/code&gt; in form-encoded data. Both are widely accepted on the way in, and that mutual tolerance is precisely why the bug survives so long: it works with the parser you tested and fails with the one in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/&lt;/code&gt; inside a path segment value.&lt;/strong&gt; Encode it as &lt;code&gt;%2F&lt;/code&gt; and the value no longer looks like two segments — but many servers decode the path &lt;em&gt;before&lt;/em&gt; routing, so &lt;code&gt;%2F&lt;/code&gt; is turned back into a separator and your value splits anyway. Some servers reject encoded slashes outright, and the reason is security: &lt;code&gt;%2F&lt;/code&gt; and &lt;code&gt;%2E&lt;/code&gt; are the building blocks of path traversal attempts, so proxies and application servers have strong opinions about them. The honest conclusion is that a value containing a slash does not belong in a single path segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;?&lt;/code&gt; in a query value.&lt;/strong&gt; Legal in a query by the grammar, and best encoded anyway, since a second &lt;code&gt;?&lt;/code&gt; invites a second round of parsing in intermediaries.&lt;/p&gt;

&lt;h1&gt;
  
  
  Non-ASCII is bytes, not characters
&lt;/h1&gt;

&lt;p&gt;This is where percent-encoding stops looking like an escaping scheme and starts looking like an encoding.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;%XX&lt;/code&gt; triplet carries &lt;strong&gt;one byte&lt;/strong&gt;, not one character. A character outside ASCII has to be turned into bytes first, and the only interoperable choice is UTF-8:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;é&lt;/code&gt; is U+00E9, two bytes in UTF-8 (&lt;code&gt;C3 A9&lt;/code&gt;), so it encodes to &lt;code&gt;%C3%A9&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A CJK character such as U+4E2D is three bytes (&lt;code&gt;E4 B8 AD&lt;/code&gt;), so it encodes to &lt;code&gt;%E4%B8%AD&lt;/code&gt; — nine characters to carry one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the producer used a different legacy encoding, the triplets are still perfectly well-formed and still decode to something — just not to the text that was sent. A GBK-encoded Chinese string arriving at a UTF-8 decoder produces a page of replacement characters, and nothing in the request looks wrong. So "the encoding is valid" and "the characters are correct" are separate questions, and the second one needs the producer's encoding to be known.&lt;/p&gt;

&lt;p&gt;The related rule for implementers: always percent-encode &lt;strong&gt;bytes&lt;/strong&gt;, never code points and never characters. In languages where a string is a sequence of UTF-16 code units, encoding a character directly is how astral-plane characters get written as surrogate halves, which no decoder will accept.&lt;/p&gt;

&lt;h1&gt;
  
  
  A decoder should refuse, not guess
&lt;/h1&gt;

&lt;p&gt;Decoding is where the damage becomes permanent, because a decoder that is generous with invalid input turns a visible bug into a plausible value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;incomplete triplet&lt;/strong&gt; at the end of the input (&lt;code&gt;%E4%B8&lt;/code&gt;) is invalid. Truncating it silently produces text that is almost right.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;invalid UTF-8 byte sequence&lt;/strong&gt; must be an error. Substituting U+FFFD without saying so means the caller never learns that the sender used the wrong encoding — and a value with a replacement character in it can be stored, indexed and passed along for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-long encodings&lt;/strong&gt; — the historical trick of writing &lt;code&gt;/&lt;/code&gt; as &lt;code&gt;%C0%AF&lt;/code&gt; — must be rejected rather than decoded to the shorter form, since that is precisely what makes them dangerous in a security context.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;%&lt;/code&gt; that is not followed by two hex digits is a malformed URL. Reporting it as malformed is more useful than treating &lt;code&gt;%&lt;/code&gt; as literal, because the caller then finds the template that forgot to escape.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A decoder that does the opposite — normalising, repairing and reporting success — is the reason two systems can disagree about the same URL while both claim to have decoded it. The strict-but-explicit behaviour is what the &lt;a href="https://toolstead.net/url-encoder/" rel="noopener noreferrer"&gt;URL encoder and decoder&lt;/a&gt; implements: it shows which characters it changed, keeps UTF-8 byte handling separate from the text, and points at the exact position of a malformed sequence instead of quietly dropping it.&lt;/p&gt;

&lt;h1&gt;
  
  
  The rules worth keeping
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Encode per component, not per URL. &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;+&lt;/code&gt; are data in one part and syntax in another.&lt;/li&gt;
&lt;li&gt;Never decode twice. &lt;code&gt;%2520&lt;/code&gt; is a percent sign followed by &lt;code&gt;20&lt;/code&gt;, not a space; if a second decode "fixes" it, the producer double-encoded, and that is their bug to fix.&lt;/li&gt;
&lt;li&gt;Compare canonical forms: uppercase hex triplets, no needlessly encoded unreserved characters, one spelling per resource.&lt;/li&gt;
&lt;li&gt;Treat non-ASCII as UTF-8 bytes and say so explicitly; where the sender's encoding is unknown, ask rather than assume.&lt;/li&gt;
&lt;li&gt;Prefer refusing malformed input to repairing it — this is the difference between a decoder and a data corruption device.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://toolstead.net/blog/url-encoding-vs-base64/" rel="noopener noreferrer"&gt;comparison with Base64&lt;/a&gt; covers which of the two layers a given payload needs; when the payload is a token that must survive a query string untouched, the &lt;a href="https://toolstead.net/blog/url-safe-base64-vs-standard/" rel="noopener noreferrer"&gt;URL-safe alphabet&lt;/a&gt; is usually the better starting point than escaping your way out of an alphabet that already contains &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>http</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Wiegand 26 vs 34 Bit Format: The Facility Code Split</title>
      <dc:creator>zbinze</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:56:48 +0000</pubDate>
      <link>https://dev.to/toolstead/wiegand-26-vs-34-bit-format-the-facility-code-split-l02</link>
      <guid>https://dev.to/toolstead/wiegand-26-vs-34-bit-format-the-facility-code-split-l02</guid>
      <description>&lt;p&gt;Wiegand is a wire, not a card format. A reader sends bits to a controller over two lines — DATA0 pulses for zeros, DATA1 pulses for ones, roughly 100 microseconds each with about a millisecond between them — and the controller reconstructs a number from the pulse count. No clock line, no packet length, no checksum. The "26" and "34" in Wiegand 26 and Wiegand 34 are simply the number of bits in that burst.&lt;/p&gt;

&lt;p&gt;The part that causes trouble is what happens &lt;em&gt;inside&lt;/em&gt; those bits. A frame carries a facility code, a card number, and two parity bits, and where the boundary between the two fields sits is a &lt;strong&gt;vendor convention&lt;/strong&gt;, not something the frame declares. Two systems can read the same card, over the same wire, and disagree about who it is — with no error anywhere.&lt;/p&gt;

&lt;h1&gt;
  
  
  The frame, bit by bit
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wiegand 26   [P][ facility 8 bits ][ card number 16 bits ][P]
Wiegand 34   [P][ facility 16 bits][ card number 16 bits ][P]

             P = parity, one bit on each end of the frame
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 26-bit layout, used by HID H10301 and most 125 kHz proximity installations, is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bit 1 — &lt;strong&gt;even parity&lt;/strong&gt; over bits 2–13&lt;/li&gt;
&lt;li&gt;bits 2–9 — &lt;strong&gt;facility code&lt;/strong&gt;, 8 bits, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;255&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;bits 10–25 — &lt;strong&gt;card number&lt;/strong&gt;, 16 bits, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;65,535&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;bit 26 — &lt;strong&gt;odd parity&lt;/strong&gt; over bits 14–25&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 34-bit layout — HID H10306 and the AWID 34-bit credentials that share its shape — moves the boundary a long way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bit 1 — &lt;strong&gt;even parity&lt;/strong&gt; over bits 2–17&lt;/li&gt;
&lt;li&gt;bits 2–17 — &lt;strong&gt;facility code&lt;/strong&gt;, 16 bits, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;65,535&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;bits 18–33 — &lt;strong&gt;card number&lt;/strong&gt;, 16 bits, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;65,535&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;bit 34 — &lt;strong&gt;odd parity&lt;/strong&gt; over bits 18–33&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the card number is the same size in both. Everything the extra eight bits buy goes into the &lt;em&gt;site&lt;/em&gt; field: 34-bit raises the number of distinct facility codes from 256 to 65,536. That is the whole difference, and it is the reason a site with more than 256 doors, tenants or buildings eventually migrates.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why the boundary cannot be guessed
&lt;/h1&gt;

&lt;p&gt;The frame length is agreed; the division is not. The same total can be split differently by different product families — 36-bit is a 17/17 site-and-card split in one, and an 8-bit site code with a 16-bit card number at entirely different bit offsets in another — and 34-bit credentials exist that use the same 16/16 shape but treat the site field as an issuer code rather than a facility code. Nothing in the transmission records which convention was used, which has a blunt consequence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A controller configured for the wrong layout does not fail. It produces a different, valid number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Feeding a 34-bit credential to a controller configured for 26 bits is the perfect example. The parser takes the first 26 bits of the burst: bit 1 becomes its even-parity bit, bits 2–9 become an 8-bit facility code that is really the top half of a 16-bit one, and bits 10–25 become a card number that is really the bottom half of the facility code followed by the top half of the real card number. The last eight bits of the frame, including the frame's own odd-parity bit, arrive after the parser has stopped reading. Almost always the parity check fails, the reader looks broken, and the card is perfectly fine.&lt;/p&gt;

&lt;p&gt;The practical version of this, which turns up far more often than a hard failure, is a &lt;em&gt;silent&lt;/em&gt; mismatch: a split that parses, passes parity and identifies a different person. With only 16 bits of card number, collisions are not theoretical. A site that has enrolled more than 65,535 credentials, or that reuses numbers across facility codes the panel can no longer see, ends up with two cards that are the same person to the controller — and the log will show a legitimate number for the wrong door.&lt;/p&gt;

&lt;h1&gt;
  
  
  Parity is the entire error-detection budget
&lt;/h1&gt;

&lt;p&gt;Two parity bits over a 32-bit payload is a thin defence, and it is worth knowing exactly how thin.&lt;/p&gt;

&lt;p&gt;Even parity over bits 2–13 means the twelve data bits plus the parity bit contain an even number of ones. If one pulse is dropped — a barrier motor starting up next to an unshielded run of cable is the classic cause — the bit count changes and the parity check fails. If &lt;strong&gt;two&lt;/strong&gt; pulses are lost inside the same parity window, the count is even again and the frame passes. Wiegand has no way to notice, because there is nothing else in the frame to cross-check against: no length field, no sequence number, no CRC.&lt;/p&gt;

&lt;p&gt;This is also why parity is computed over halves of the frame rather than the whole payload. Two 12-bit windows catch error patterns that one 26-bit window would not, at the cost of the two bits themselves. It is a 1950s answer to a 1950s problem, and it survives because the alternative — a bidirectional protocol — requires wires that the installed base does not have.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reading a number out of a frame
&lt;/h1&gt;

&lt;p&gt;Once a controller has the frame, the number it stores is the card number field, usually printed as decimal. That is where card-number formats start to blur: the same credential may be printed by one tool as a 26-bit decimal card number, by another as the raw hex of the whole frame — hex being a 1:1 representation of the bytes rather than an encoding, as &lt;a href="https://toolstead.net/blog/base64-vs-hex-base32-ascii85/" rel="noopener noreferrer"&gt;the comparison of hex, Base32, Base64 and Ascii85&lt;/a&gt; sets out — and by a third as a UID in reversed byte order. If the numbers you have in hand do not line up, the field boundary and the byte order are separate questions — &lt;a href="https://toolstead.net/blog/card-number-formats-explained/" rel="noopener noreferrer"&gt;Card Number Formats Explained&lt;/a&gt; walks the order to check them in, and the &lt;a href="https://toolstead.net/card-number-converter/" rel="noopener noreferrer"&gt;card number converter&lt;/a&gt; will show every form of a value you paste in, including the reversed reading that explains most "the system has the wrong number" tickets.&lt;/p&gt;

&lt;p&gt;One useful sanity check before any of that: a 26-bit card number cannot exceed &lt;code&gt;65,535&lt;/code&gt;. If the number in your database is larger, it did not come from a 26-bit card number field — it came from a raw UID, a 34-bit-plus layout, or a mis-split frame.&lt;/p&gt;

&lt;p&gt;When you want the frames computed rather than explained, the &lt;a href="https://toolstead.net/wiegand-26-34-converter/" rel="noopener noreferrer"&gt;Wiegand 26/34 converter&lt;/a&gt; takes a whole frame, a hex payload, a facility-code pair or a bare card number and shows both widths side by side — including the parity check that tells you whether a captured frame survived the wiring.&lt;/p&gt;

&lt;h1&gt;
  
  
  You cannot convert 26 to 34
&lt;/h1&gt;

&lt;p&gt;There is no arithmetic that turns a 26-bit credential into a 34-bit one, because nothing is being computed — a field boundary is being moved. Two honest options exist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reconfigure the panel&lt;/strong&gt; to the format the credentials already use. This is a controller setting, and it is the cheap path when the reader, the cards and the panel all support the layout you are switching to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-enroll&lt;/strong&gt; the credentials into the new split, which means re-issuing or re-reading every card and rebuilding the access lists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What does not work is "converting" the numbers: mapping a 900-series card number into a different facility code produces a credential that no card in the building carries. If a migration is on the table, the audit worth doing first is a count of distinct facility codes and the highest card number in use — those two numbers decide whether 34-bit is needed or merely convenient.&lt;/p&gt;

&lt;h1&gt;
  
  
  The security footnote that outlives the format
&lt;/h1&gt;

&lt;p&gt;Wiegand is one-directional and unauthenticated. The reader talks; the controller listens; nothing is encrypted, and a facility code is printed on the card and known to anyone who has ever ordered credentials. 125 kHz proximity cards are also trivially clonable with commodity hardware, and because card numbers are usually sequential, an attacker who captures one frame has a good guess at its neighbours.&lt;/p&gt;

&lt;p&gt;None of that is fixed by moving to 34 bits — it is fixed by replacing the protocol, which is what OSDP does: bidirectional, with a secure channel and encryption, so a reader can be authenticated and a cloned card alone is not enough. The reason 26-bit is still everywhere is not that it is good; it is that the wire, the readers and the doors are already installed, and the format travels with them.&lt;/p&gt;

&lt;p&gt;If you are debugging a credential that reads differently on two systems, start with the bit split and the byte order — in that order. The frames are not lying; they are answering a slightly different question than the one being asked.&lt;/p&gt;

</description>
      <category>security</category>
      <category>hardware</category>
      <category>programming</category>
      <category>embedded</category>
    </item>
    <item>
      <title>Base64 Image Not Showing? Six Checks, Cheapest First</title>
      <dc:creator>zbinze</dc:creator>
      <pubDate>Sun, 25 Oct 2026 00:09:01 +0000</pubDate>
      <link>https://dev.to/toolstead/base64-image-not-showing-six-checks-cheapest-first-2be1</link>
      <guid>https://dev.to/toolstead/base64-image-not-showing-six-checks-cheapest-first-2be1</guid>
      <description>&lt;p&gt;A Base64 image that will not display is one of the least informative failures in web development. You get a broken-image icon, or an empty box, or nothing at all — and the browser console is silent. There is no exception to catch, because the browser did exactly what it was told: it tried to render a resource and gave up quietly.&lt;/p&gt;

&lt;p&gt;The good news is that there are only six realistic causes, they are cheap to check in order, and each one produces a different symptom if you look closely.&lt;/p&gt;

&lt;h1&gt;
  
  
  Check 1: is the string actually Base64?
&lt;/h1&gt;

&lt;p&gt;Before anything else, establish that the payload is in the right alphabet. Two count-based checks catch most of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;length % 4 =&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                        &lt;span class="c1"&gt;// 1 means truncated&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;illegal =&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9+&lt;/span&gt;&lt;span class="se"&gt;/&lt;/span&gt;&lt;span class="sr"&gt;=_-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A remainder of 1 is fatal: no complete Base64 string can produce it, so characters were lost in transit. Everything else can usually be repaired. Illegal characters tell you &lt;em&gt;what&lt;/em&gt; went wrong — a newline means MIME wrapping (see &lt;a href="https://toolstead.net/blog/base64-line-breaks-padding/" rel="noopener noreferrer"&gt;line breaks and padding&lt;/a&gt;), a &lt;code&gt;%3D&lt;/code&gt; means the string went through a URL, and a &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt; means it is URL-safe Base64 (see &lt;a href="https://toolstead.net/blog/url-safe-base64-vs-standard/" rel="noopener noreferrer"&gt;URL-safe vs standard&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If the string is full of &lt;code&gt;0-9a-f&lt;/code&gt; and nothing else, it is hexadecimal. Decoding it as Base64 will produce bytes without error, and those bytes will not be an image.&lt;/p&gt;

&lt;h1&gt;
  
  
  Check 2: is the prefix complete?
&lt;/h1&gt;

&lt;p&gt;The syntax is &lt;code&gt;data:[&amp;lt;mediatype&amp;gt;][;base64],&amp;lt;data&amp;gt;&lt;/code&gt; and all three parts are load-bearing. Three prefix bugs produce three different symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;;base64&lt;/code&gt; flag.&lt;/strong&gt; &lt;code&gt;data:image/png,iVBORw0KGgo...&lt;/code&gt; makes the browser read the payload as literal text. You get an empty image box and no error, because you asked for a text file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No comma.&lt;/strong&gt; &lt;code&gt;data:image/png;base64&lt;/code&gt; followed by the payload is not a data URI at all. If your code strips "everything up to the first comma", the strip silently does nothing and the decoder chokes on the &lt;code&gt;:&lt;/code&gt; in the header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;;base64&lt;/code&gt; typo&lt;/strong&gt; such as &lt;code&gt;base64;&lt;/code&gt; or &lt;code&gt;;base-64&lt;/code&gt;. Same as the first case: the payload is treated as text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a strict decoder these all fail loudly at a specific character position. In a browser they fail silently. That asymmetry is worth remembering when debugging: the browser is the least helpful place to ask "why".&lt;/p&gt;

&lt;h1&gt;
  
  
  Check 3: did it decode to any bytes at all?
&lt;/h1&gt;

&lt;p&gt;A zero-byte result is a real outcome, and it has a specific cause: the payload contained nothing but padding characters, or the normalisation step removed everything it considered junk. Print the decoded length before you attempt to render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0 → nothing to render, however good the string looks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the length is in the hundreds of kilobytes, you have data. Whether it is an image is Check 4.&lt;/p&gt;

&lt;h1&gt;
  
  
  Check 4: are the bytes an image — and the right one?
&lt;/h1&gt;

&lt;p&gt;This is where the declared media type gets tested against reality. Read the first bytes and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89 50        PNG
FF D8 FF     JPEG
47 49 46 38  GIF
52 49 46 46  WebP (with "WEBP" at offset 8)
1F 8B        gzip — not an image
25 50 44 46  PDF
50 4B 03 04  ZIP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two distinct failures live here. The first is that the bytes are &lt;strong&gt;not an image at all&lt;/strong&gt; — gzipped JSON that was Base64-encoded to move it through a text-only channel is the classic case, and it is worth naming explicitly because "it decoded fine" makes people stop looking. The second is that the bytes &lt;strong&gt;are&lt;/strong&gt; an image but a different one from what the header claims: &lt;code&gt;data:image/jpeg;base64,iVBORw0KGgo...&lt;/code&gt; renders in most browsers, then fails in every tool that trusts the declared type. The bytes win in both cases, which is why our &lt;a href="https://toolstead.net/base64-to-image/" rel="noopener noreferrer"&gt;Base64 to Image&lt;/a&gt; tool prints the declared type and the signature side by side instead of picking one.&lt;/p&gt;

&lt;h1&gt;
  
  
  Check 5: is the image complete?
&lt;/h1&gt;

&lt;p&gt;This is the cause people miss, and it produces the most confusing symptom: the image &lt;strong&gt;does&lt;/strong&gt; display, then stops halfway. A grey band across the bottom of a photo, or a picture that renders correctly in a browser but is rejected by an image editor.&lt;/p&gt;

&lt;p&gt;The reason is that JPEG has no total-length field in its header. A JPEG truncated at any point still looks like a perfectly valid JPEG as long as the markers parsed so far were legal, so decoders render what they have and stop. Truncation is common because Base64 travels through places that truncate: log lines with length caps, terminal selection that drops a wrapped line, database columns sized for the average case, chat messages with attachment limits.&lt;/p&gt;

&lt;p&gt;The test is arithmetic, not visual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the string and record the byte count.&lt;/li&gt;
&lt;li&gt;Compare it with the size the source reports for the original file.&lt;/li&gt;
&lt;li&gt;If the decoded count is lower, you have a truncated copy — go back to the source rather than trying to repair it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For PNG the same check works, and there is an extra structural signal: a complete PNG ends with an &lt;code&gt;IEND&lt;/code&gt; chunk. Reading the last twelve bytes and looking for &lt;code&gt;49 45 4E 44&lt;/code&gt; (&lt;code&gt;IEND&lt;/code&gt;) is a cheap completeness test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IEND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Check 6: is the browser refusing it for a reason unrelated to Base64?
&lt;/h1&gt;

&lt;p&gt;The last two causes have nothing to do with the string, which is exactly why they are missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Security Policy.&lt;/strong&gt; If your page sends &lt;code&gt;img-src 'self'&lt;/code&gt; without &lt;code&gt;data:&lt;/code&gt;, every data URI image is blocked — silently, and with no Base64 problem anywhere. The browser reports the violation to the console in most cases, but only if you know to look there, and tools that inject their own CSP by default (static hosts, reverse proxies) can add this without you noticing. Our own pages ship &lt;code&gt;img-src 'self' blob: data:&lt;/code&gt; precisely for this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size.&lt;/strong&gt; Data URIs are string data and live inside whatever document contains them. Very large ones — a few megabytes of Base64 in an inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; or a generated HTML file — hit parser and memory limits that vary by browser, and the failure looks like nothing at all happening. If a 40 KB icon works and a 4 MB photo does not, this is your answer, and the fix is a real URL instead.&lt;/p&gt;

&lt;h1&gt;
  
  
  The order matters
&lt;/h1&gt;

&lt;p&gt;Run the checks in this order and stop at the first failure, because the later ones produce misleading results when the earlier ones are broken: a truncated string appears to "decode fine" and a wrong alphabet produces bytes that look like a corrupt image. Six checks, roughly a minute of work, and each one tells you a different thing to fix.&lt;/p&gt;

&lt;p&gt;When you have the answer, the two repairs worth knowing are both structural rather than textual: pad the string if the remainder says it is short (safe, reversible), and go back to the source if the remainder is 1 or the decoded length does not match what the producer reported (not safe, not reversible, do not guess). Anything else — re-encoding, "trying" a different alphabet, appending bytes until it renders — is guessing at data you cannot see.&lt;/p&gt;

</description>
      <category>base64</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
