<?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: zhihu wu</title>
    <description>The latest articles on DEV Community by zhihu wu (@zhihu_wu_dea1d82af01a04d7).</description>
    <link>https://dev.to/zhihu_wu_dea1d82af01a04d7</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%2F3921997%2Fa775d61f-ed57-461b-ac46-ed108350189e.png</url>
      <title>DEV Community: zhihu wu</title>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhihu_wu_dea1d82af01a04d7"/>
    <language>en</language>
    <item>
      <title>Why Your Diff Shows Every Line Changed (And It's Not Your Code)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:02:11 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-diff-shows-every-line-changed-and-its-not-your-code-19f8</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-diff-shows-every-line-changed-and-its-not-your-code-19f8</guid>
      <description>&lt;p&gt;You open a pull request. You changed one line. The diff is 412 lines.&lt;/p&gt;

&lt;p&gt;You blame the linter, then yourself. Usually it's neither. A diff compares characters before it compares meaning, and the characters that changed are invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Line endings: CRLF vs LF
&lt;/h2&gt;

&lt;p&gt;The classic culprit. A file written on Linux ends its lines with &lt;code&gt;\n&lt;/code&gt;; a file written on Windows ends them with &lt;code&gt;\r\n&lt;/code&gt;. Git stores bytes, so the moment a teammate's editor (or a script, or a Windows tool) rewrites the file with the other convention, every single line differs by one byte you cannot see.&lt;/p&gt;

&lt;p&gt;To confirm it, run &lt;code&gt;git diff --ignore-cr-at-eol&lt;/code&gt;. If the 412-line diff collapses to your one real change, that's your answer. When the two versions are not in git yet — two config files, two API response dumps — I paste both into the &lt;a href="https://codetoolbox.pro/tools/diff-checker.html" rel="noopener noreferrer"&gt;Diff Checker&lt;/a&gt; and flip the "ignore whitespace" toggle. It runs entirely in the browser, so pasting a private config is not a concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Trailing whitespace
&lt;/h2&gt;

&lt;p&gt;Editors with "trim trailing whitespace on save" enabled rewrite every line they touch, and any formatter run rewrites the whole file. You get lines marked as changed that look byte-identical, because the difference sits at the end, past where your eye stops rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Tabs vs spaces, and indent conversion
&lt;/h2&gt;

&lt;p&gt;One contributor's editor converts indentation on save. Every nested block now reads as "modified". &lt;code&gt;git diff -w&lt;/code&gt; (ignore all whitespace) collapses it instantly — and if it does, you know the change is cosmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Encoding and BOM
&lt;/h2&gt;

&lt;p&gt;A BOM added by a Windows editor, or a latin-1 → UTF-8 rewrite, changes every line containing a non-ASCII character. &lt;code&gt;file&lt;/code&gt; on both versions tells you the encoding, and a suspiciously huge &lt;code&gt;git diff --stat&lt;/code&gt; on a mostly-ASCII file is the hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that fixes it permanently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.gitattributes&lt;/code&gt; with &lt;code&gt;* text=auto eol=lf&lt;/code&gt; normalizes line endings at commit time, once, for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.editorconfig&lt;/code&gt; with &lt;code&gt;end_of_line = lf&lt;/code&gt;, &lt;code&gt;insert_final_newline = true&lt;/code&gt;, &lt;code&gt;trim_trailing_whitespace = true&lt;/code&gt; so every editor agrees on the rules.&lt;/li&gt;
&lt;li&gt;Review with &lt;code&gt;git diff -w&lt;/code&gt; before you push. If the diff disappears, the change is whitespace-only: revert it or commit it on its own, so the real change stays reviewable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A diff never lies — it compares characters. When its report looks impossible, check the invisible ones first: end-of-line bytes, trailing spaces, indentation, encoding. A whitespace-aware diff view is the fastest way to stop guessing and get back to your actual change.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>git</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Day Is Not 86400 Seconds: The DST Bug in Your Date Math</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:02:29 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/a-day-is-not-86400-seconds-the-dst-bug-in-your-date-math-3bc0</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/a-day-is-not-86400-seconds-the-dst-bug-in-your-date-math-3bc0</guid>
      <description>&lt;p&gt;Most "date bugs" I've had to chase in production weren't off by a timezone. They were off by exactly one day, and they only appeared twice a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "tomorrow, same time"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tomorrow&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code is correct 363 days a year. On the other two it is off by an hour, and if you round the result down to a date it is off by a whole day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The epoch is absolute; calendar units are not
&lt;/h2&gt;

&lt;p&gt;A Unix timestamp is a fixed instant. &lt;code&gt;1758700800&lt;/code&gt; is the same moment in Lagos, Lisbon and Lima - only the rendered local time differs. Arithmetic in seconds is therefore always exact in absolute time, and that is the trap: "one day later" is not an absolute duration. It is a calendar operation whose length depends on the timezone rules in effect at both moments.&lt;/p&gt;

&lt;p&gt;A spring-forward day is 82,800 seconds long; a fall-back day is 90,000. Add exactly 86,400 seconds to local midnight and you land at 01:00 (already past the boundary you wanted) or at 23:00 of the day before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same bug, four languages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// adds exactly 86400 s - wrong across a DST boundary&lt;/span&gt;
&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;plus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// calendar-aware - what you usually mean&lt;/span&gt;
&lt;span class="nc"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;plusDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// +86400 s exactly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&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="c1"&gt;// one calendar day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go's &lt;code&gt;t.Add(24 * time.Hour)&lt;/code&gt; is a duration; &lt;code&gt;t.AddDate(0, 0, 1)&lt;/code&gt; is a calendar day. In SQL, &lt;code&gt;date_add(d, interval 1 day)&lt;/code&gt; follows the zone while &lt;code&gt;d + interval 86400 second&lt;/code&gt; does not. Python is the sneakiest: &lt;code&gt;dt + timedelta(days=1)&lt;/code&gt; is wall-clock arithmetic on naive datetimes, but adding timedelta to an aware datetime in DST-aware zones can still surprise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixes it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Store the absolute instant - epoch seconds or UTC ISO 8601. Timestamps do not have DST.&lt;/li&gt;
&lt;li&gt;Make "which calendar day is this?" decisions in the user's timezone (the zone, not an offset: an offset is a snapshot, a zone is a rule set with a history).&lt;/li&gt;
&lt;li&gt;For "yesterday's rows", compute the day boundaries as calendar days in that zone and convert those boundaries to instants - instead of subtracting 86400 from now.&lt;/li&gt;
&lt;li&gt;Put a DST date in your test suite. Twice a year is exactly often enough to forget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When one of these bites, the first move is converting the raw epoch values in the logs to local time. I use the &lt;a href="https://codetoolbox.pro/tools/timestamp-converter.html" rel="noopener noreferrer"&gt;timestamp converter on CodeToolbox&lt;/a&gt; for that - it shows local and UTC side by side and auto-detects seconds vs milliseconds, which is the other half of this bug family, and it runs entirely in the browser so staging log values never leave the machine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why the Same Markdown Renders Differently on GitHub, Dev.to, and Notion</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Mon, 21 Sep 2026 13:02:35 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-the-same-markdown-renders-differently-on-github-devto-and-notion-3fgi</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-the-same-markdown-renders-differently-on-github-devto-and-notion-3fgi</guid>
      <description>&lt;p&gt;You write &lt;code&gt;| Name | Role |&lt;/code&gt; with a clean separator row, commit, and open the README. On GitHub it's a tidy table. In your team's docs site it's a wall of pipes. Same file, same lines, different renderer.&lt;/p&gt;

&lt;p&gt;That's not a bug in your Markdown. "Markdown" stopped being one language years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three dialects you hit every week
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CommonMark&lt;/strong&gt; — the spec. Headings, lists, emphasis, links, code blocks, blockquotes, and not much else. If a renderer says "strict Markdown," this is what it means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GFM (GitHub Flavored Markdown)&lt;/strong&gt; — CommonMark plus tables, task lists, strikethrough, autolinked URLs, and footnotes. GitHub uses it. So does Dev.to, most static site generators (Jekyll, Hugo, MkDocs with the right plugin), and marked.js with remark-gfm in the JavaScript ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editor dialects&lt;/strong&gt; — Notion, Obsidian and friends add callouts, &lt;code&gt;[[wikilinks]]&lt;/code&gt;, highlight marks and databases. They export Markdown, but the round trip is lossy: a Notion callout becomes a plain paragraph the moment it lands in someone else's editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the divergence actually bites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tables.&lt;/strong&gt; GFM only. Without it, you get raw pipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task lists.&lt;/strong&gt; &lt;code&gt;- [x]&lt;/code&gt; renders as a checkbox on GitHub; elsewhere it's the literal text &lt;code&gt;[x]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emoji shortcodes.&lt;/strong&gt; &lt;code&gt;:shipit:&lt;/code&gt; needs an emoji plugin. Core parsers leave it alone — paste the real character instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line breaks.&lt;/strong&gt; A single newline is a space in CommonMark. GitHub behaves the same way, which is why "my newline does nothing" is usually correct behavior rather than a broken preview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heading anchors.&lt;/strong&gt; GitHub slugifies headings into anchors (&lt;code&gt;## Setup Guide&lt;/code&gt; becomes &lt;code&gt;#setup-guide&lt;/code&gt;). Other renderers generate different slugs, so cross-links break silently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical habit is to write for the smallest common subset: assume CommonMark plus tables and task lists, and treat everything else as platform-specific sugar. Then verify before publishing — not in your editor, but in a renderer that matches your audience.&lt;/p&gt;

&lt;p&gt;For that last step I keep a browser previewer open: it runs marked.js with GFM enabled (the same dialect GitHub and Dev.to use), renders as you type, and never uploads a byte — everything happens client-side. Here it is: &lt;a href="https://codetoolbox.pro/tools/markdown-preview.html" rel="noopener noreferrer"&gt;Markdown Preview on CodeToolbox&lt;/a&gt;. Paste the file, confirm your tables and task lists survive the trip, then commit.&lt;/p&gt;

&lt;p&gt;One minute of previewing beats a "fix README formatting" commit at 11pm.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>github</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your JWT Is Not Encrypted - Here's What's Actually Inside It</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sat, 19 Sep 2026 13:02:58 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-jwt-is-not-encrypted-heres-whats-actually-inside-it-19ik</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-jwt-is-not-encrypted-heres-whats-actually-inside-it-19ik</guid>
      <description>&lt;p&gt;A JWT looks like encrypted gibberish: &lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx...&lt;/code&gt;. Here is the uncomfortable part — it is not encrypted. Anyone holding the token can read everything in it. That is by design, and it is also why a surprising number of "why is auth broken?" bugs get solved in about ten seconds once you actually look inside the token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three segments, no secrets
&lt;/h2&gt;

&lt;p&gt;A JWT is &lt;code&gt;header.payload.signature&lt;/code&gt;, each part Base64URL-encoded, joined by dots. Base64URL is just an encoding (URL-safe alphabet, padding stripped so it can live in a header, cookie, or query string). The first two segments are plain JSON. The third is a signature over the first two — a signature proves nobody tampered with the token, it does not hide anything.&lt;/p&gt;

&lt;p&gt;Decode one in Node, no dependencies:&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc123&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&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;payload&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="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;claims&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&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;payload&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="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to check when auth fails
&lt;/h2&gt;

&lt;p&gt;Almost every JWT bug is one of these claims, and they are all Unix seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt; — expiry. Expired tokens produce a 401 that has nothing to do with your code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nbf&lt;/code&gt; — "not before". A token used too early is invalid, which bites when servers' clocks drift.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; / &lt;code&gt;iss&lt;/code&gt; — audience and issuer. One service rejecting a token issued for a different audience is extremely common in multi-service setups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convert &lt;code&gt;exp&lt;/code&gt; and compare against the clock before you start rewriting middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that is not a bug: alg: none
&lt;/h2&gt;

&lt;p&gt;The header carries &lt;code&gt;alg&lt;/code&gt;, and the single worst JWT mistake is letting the client tell the server which algorithm to trust. If a server honours &lt;code&gt;"alg": "none"&lt;/code&gt;, an attacker deletes the signature and forges any payload they want. A subtler variant is algorithm confusion: an RS256 token re-signed as HS256 using the public key as the HMAC secret. Fix both by pinning the accepted algorithms server-side and always verifying with a real library (&lt;code&gt;jose&lt;/code&gt;, &lt;code&gt;PyJWT&lt;/code&gt;, &lt;code&gt;jsonwebtoken&lt;/code&gt;) — never by decoding and trusting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't put anything private in the payload
&lt;/h2&gt;

&lt;p&gt;Roles, internal IDs, emails, feature flags — all readable by whoever holds the token, including anyone who finds it in a log or a URL. Keep the payload minimal; look up the rest server-side.&lt;/p&gt;

&lt;p&gt;When I'm debugging one of these, I decode the token first. I've been using &lt;a href="https://codetoolbox.pro/tools/jwt-decoder.html" rel="noopener noreferrer"&gt;CodeToolbox's JWT decoder&lt;/a&gt; — it runs entirely in the browser (no token leaves your machine), prints the header and claims, and converts &lt;code&gt;exp&lt;/code&gt;/&lt;code&gt;iat&lt;/code&gt;/&lt;code&gt;nbf&lt;/code&gt; into readable local time so I can see instantly whether the token is expired. It does not verify signatures, which is the honest limitation of any browser-side tool: verification needs the key and belongs on your server.&lt;/p&gt;

&lt;p&gt;Token debugging tip of the day: read the claims before you touch the code. Most of the time, the token was simply expired.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your Regex Returns true Then false on the Same Input</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:01:44 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-regex-returns-true-then-false-on-the-same-input-3b2o</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-regex-returns-true-then-false-on-the-same-input-3b2o</guid>
      <description>&lt;p&gt;You write a quick validation loop, run it, and get &lt;code&gt;true, false, true, false&lt;/code&gt; for four identical strings. Nothing in the input changed. Nothing in the pattern changed. So what did?&lt;/p&gt;

&lt;p&gt;The regex object is carrying state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden &lt;code&gt;lastIndex&lt;/code&gt; property
&lt;/h2&gt;

&lt;p&gt;When a pattern has the &lt;code&gt;g&lt;/code&gt; flag, the returned &lt;code&gt;RegExp&lt;/code&gt; object keeps a property called &lt;code&gt;lastIndex&lt;/code&gt; — the position where the previous match ended. The next &lt;code&gt;.test()&lt;/code&gt; or &lt;code&gt;.exec()&lt;/code&gt; call starts searching from there instead of from zero. On a successful match &lt;code&gt;lastIndex&lt;/code&gt; moves forward; on a failure it resets to 0. Because &lt;code&gt;test()&lt;/code&gt; only hands you a boolean, that reset is invisible, and you see just the symptom: the same input reporting different results depending on what came before it.&lt;/p&gt;

&lt;p&gt;It bites hardest in exactly the places regex lives longest — a module-level constant, a class field, a cached pattern inside a helper. A regex compiled inline inside a loop behaves fine because it gets thrown away after each pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to fix it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Compile fresh each time.&lt;/strong&gt; A new regex object starts with &lt;code&gt;lastIndex = 0&lt;/code&gt;:&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;function&lt;/span&gt; &lt;span class="nf"&gt;isHexColor&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^#[0-9a-f]{6}$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Reset it manually&lt;/strong&gt; if you want to keep reusing one object for performance:&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;HEX&lt;/span&gt; &lt;span class="o"&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;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{6}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isHexColor&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;HEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;HEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Use &lt;code&gt;matchAll()&lt;/code&gt;&lt;/strong&gt; when you want every match. It returns an iterator and does not mutate the original pattern, so no index bookkeeping leaks into your logic:&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;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a1 b2 c3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&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-z&lt;/span&gt;&lt;span class="se"&gt;]([&lt;/span&gt;&lt;span class="sr"&gt;0-9&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The sticky flag too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;y&lt;/code&gt; (sticky) behaves the same way — it also anchors matching at &lt;code&gt;lastIndex&lt;/code&gt;, and it does not reset on failure. If you assemble flag strings dynamically from user input or config, that state can survive far longer than you expect.&lt;/p&gt;

&lt;p&gt;Rule of thumb: treat any &lt;code&gt;g&lt;/code&gt;- or &lt;code&gt;y&lt;/code&gt;-flagged regex as a stateful object. Either create one per use, or set &lt;code&gt;lastIndex = 0&lt;/code&gt; at the top of every call.&lt;/p&gt;

&lt;p&gt;💡 When you are chasing this class of bug, seeing every match highlighted as you type beats sprinkling &lt;code&gt;console.log&lt;/code&gt; through the file. I keep a regex tester open in a browser tab (there's a free one at codetoolbox.pro/tools/regex-tester.html) — it recompiles the pattern on each keystroke, shows capture groups side by side, and runs entirely in your browser, so nothing you paste in leaves your machine.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How Fast Can a GPU Crack Your Password? Let's Do the Math</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:02:40 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/how-fast-can-a-gpu-crack-your-password-lets-do-the-math-3hjc</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/how-fast-can-a-gpu-crack-your-password-lets-do-the-math-3hjc</guid>
      <description>&lt;p&gt;Every few months someone posts a "this password takes 3 billion years to crack" chart, and every few months it's wrong — not because the math is hard, but because the chart quietly assumes the site you're logging into stored your password the way it should have.&lt;/p&gt;

&lt;p&gt;Here's the only equation that matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;keyspace ÷ guesses per second = time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both numbers are knowable. Let's fill them in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a single modern GPU actually does
&lt;/h2&gt;

&lt;p&gt;Rough hashcat figures for one RTX 4090 — orders of magnitude are what matter here, not decimal places:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What's being attacked&lt;/th&gt;
&lt;th&gt;Guesses per second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MD5&lt;/td&gt;
&lt;td&gt;~100,000,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NTLM (Active Directory, Windows)&lt;/td&gt;
&lt;td&gt;comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt, cost 12&lt;/td&gt;
&lt;td&gt;~20,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2id, sensibly tuned&lt;/td&gt;
&lt;td&gt;a few thousand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a five-million-fold spread. Which hash a site uses changes the cracking time far more than an extra character does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three real answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Eight lowercase letters and digits&lt;/strong&gt; — 36^8 ≈ 2.8 trillion candidates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Against bcrypt at 20,000/s: &lt;strong&gt;~4.5 years&lt;/strong&gt; on one GPU.&lt;/li&gt;
&lt;li&gt;Against MD5 at 100 billion/s: &lt;strong&gt;~28 seconds.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Four random words&lt;/strong&gt; from a 7,776-word list — 7776^4 ≈ 3.7 quadrillion candidates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Against bcrypt: &lt;strong&gt;~5,800 years.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Against MD5: &lt;strong&gt;~10 hours.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Rent ten GPUs&lt;/strong&gt; and divide by ten. Cloud GPU is billed by the minute; nobody needs to own the hardware to run this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually tells you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Length is cheap, symbols are expensive.&lt;/strong&gt; Going 8 → 12 characters costs the attacker thousands of times more; swapping &lt;code&gt;a&lt;/code&gt; for &lt;code&gt;@&lt;/code&gt; multiplies keyspace by a handful. Passphrases buy more than leetspeak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot control the hash.&lt;/strong&gt; You only find out what a service uses after it leaks. Assume worst case and make the keyspace big enough to survive it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online attacks are a different problem.&lt;/strong&gt; Login forms are rate-limited, locked out, and often behind 2FA — the math above applies to an offline dump of hashes, which is how breaches actually burn people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse is the multiplier.&lt;/strong&gt; One reused password turns one breach into twenty compromised accounts, no cracking required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The boring habit that works
&lt;/h2&gt;

&lt;p&gt;Generate 16+ random characters, unique per site, and let a password manager hold them. The only thing you need to remember is the manager's passphrase — and that one deserves the passphrase treatment, because it's the single key that unlocks everything else.&lt;/p&gt;

&lt;p&gt;When I want to see the difference between my old 8-character habit and what a generator produces, I use &lt;a href="https://codetoolbox.pro/tools/password-generator.html" rel="noopener noreferrer"&gt;CodeToolbox's password generator&lt;/a&gt; — it runs entirely in your browser with no network calls, and it shows the entropy estimate next to the output. Watching that number jump from ~40 bits to ~100+ just by picking length instead of complexity is a better teacher than any security blog post, including this one.&lt;/p&gt;

&lt;p&gt;Pick length. Skip the symbol gymnastics.&lt;/p&gt;

</description>
      <category>security</category>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Contrast Ratio Bug Hiding in Your Color Palette</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:01:35 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/the-contrast-ratio-bug-hiding-in-your-color-palette-1lb4</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/the-contrast-ratio-bug-hiding-in-your-color-palette-1lb4</guid>
      <description>&lt;p&gt;Most accessibility bugs I've shipped were not missing alt text or broken keyboard order. They were colors.&lt;/p&gt;

&lt;p&gt;A brand blue that looked fine in Figma, dropped onto a button, with white label text that nobody could read on a laptop at 40% brightness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contrast is a number, not a vibe
&lt;/h2&gt;

&lt;p&gt;WCAG 2.1 defines contrast as a ratio between the relative luminance of two colors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L = 0.2126 * R + 0.7152 * G + 0.0722 * B
contrast = (L1 + 0.05) / (L2 + 0.05)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R, G and B have to be linearized first (undo the sRGB gamma curve), which is why you cannot just average RGB values and call it done. The thresholds are 4.5:1 for normal text, 3:1 for large text (18px+, or 14px bold) and UI component boundaries.&lt;/p&gt;

&lt;p&gt;Three traps that keep sailing through design review:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gray body text.&lt;/strong&gt; &lt;code&gt;#999999&lt;/code&gt; on white is about 2.8:1 and fails. &lt;code&gt;#767676&lt;/code&gt; is the lightest gray that still passes on white.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colored text on a colored background.&lt;/strong&gt; Dark blue on medium blue can measure 1.6:1 even though both colors clearly "look different."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placeholder text.&lt;/strong&gt; Usually the same light gray as trap 1, and it is text the user has to read to fill in the form.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  HSL lightness lies to you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hsl(60, 100%, 50%)&lt;/code&gt; (yellow) and &lt;code&gt;hsl(240, 100%, 50%)&lt;/code&gt; (blue) share an identical lightness value and have wildly different perceived brightness. That is because HSL lightness is a geometric property of the RGB cube, not a measure of how much light actually reaches your eye.&lt;/p&gt;

&lt;p&gt;When you need a fast estimate, use perceived brightness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Y = (0.299 * R + 0.587 * G + 0.114 * B) / 255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above roughly 0.6 the background is light, so use dark text. Below 0.4, use white. The 0.4-0.6 band is where both options look muddy, and that is your signal to change the background instead of fighting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30-second workflow
&lt;/h2&gt;

&lt;p&gt;Pick the background, read off the RGB values, and check the ratio before you hand off to QA. I use CodeToolbox's color picker (&lt;a href="https://codetoolbox.pro/tools/color-picker.html" rel="noopener noreferrer"&gt;https://codetoolbox.pro/tools/color-picker.html&lt;/a&gt;) because it shows HEX, RGB and HSL side by side and updates live as you drag a slider, so you can nudge a color until it crosses the threshold without converting anything by hand. It runs entirely in your browser, no uploads, no signup.&lt;/p&gt;

&lt;p&gt;Contrast is one of the few accessibility requirements you can satisfy completely and deterministically in a single commit. Might as well.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>a11y</category>
      <category>design</category>
    </item>
    <item>
      <title>Unix Timestamps: The Seconds-vs-Milliseconds Bug That Wastes Hours</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:09:25 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/unix-timestamps-the-seconds-vs-milliseconds-bug-that-wastes-hours-6cf</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/unix-timestamps-the-seconds-vs-milliseconds-bug-that-wastes-hours-6cf</guid>
      <description>&lt;p&gt;Every developer has hit this at least once: a timestamp that renders as &lt;strong&gt;1970-01-01&lt;/strong&gt; or, even weirder, as the year &lt;strong&gt;5138&lt;/strong&gt;. The system isn't broken — you're mixing seconds and milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core distinction
&lt;/h2&gt;

&lt;p&gt;A Unix timestamp counts seconds since the epoch (1970-01-01 UTC). &lt;strong&gt;Seconds&lt;/strong&gt; values are 10 digits (&lt;code&gt;1728000000&lt;/code&gt;). &lt;strong&gt;Milliseconds&lt;/strong&gt; values are 13 digits (&lt;code&gt;1728000000000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The trap: JavaScript, Java, and .NET natively produce &lt;strong&gt;milliseconds&lt;/strong&gt; (&lt;code&gt;Date.now()&lt;/code&gt;), while Go, Python, PHP, and nearly every database work in &lt;strong&gt;seconds&lt;/strong&gt;. Pass a 13-digit value where 10 digits are expected and you've just asked the API to interpret a date 1000× too far in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this bites in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JWT expiry.&lt;/strong&gt; JWT claims carry &lt;code&gt;exp&lt;/code&gt; and &lt;code&gt;iat&lt;/code&gt; in &lt;em&gt;seconds&lt;/em&gt;. Generate them with &lt;code&gt;Date.now() + 3600000&lt;/code&gt; instead of &lt;code&gt;Math.floor(Date.now()/1000) + 3600&lt;/code&gt; and your token expires... sometime around the year 5138, which most servers treat as already-expired or never-expiring depending on validation logic. Auth bugs that make no sense usually trace back here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API timestamps.&lt;/strong&gt; Many JSON APIs return raw Unix seconds. When debugging, logging &lt;code&gt;"created_at": 1756789200&lt;/code&gt; to a console that doesn't auto-format is useless — you have to convert it in your head or open a converter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database queries.&lt;/strong&gt; MySQL's &lt;code&gt;UNIX_TIMESTAMP()&lt;/code&gt; returns seconds; if your app layer sends milliseconds, your range queries silently return nothing for "today."&lt;/p&gt;

&lt;h2&gt;
  
  
  A fast mental check
&lt;/h2&gt;

&lt;p&gt;Count the digits. &lt;strong&gt;10 digits = seconds, 13 = milliseconds.&lt;/strong&gt; If a value has 12 digits, it's almost certainly milliseconds from a truncated or rounded source — treat it with suspicion.&lt;/p&gt;

&lt;p&gt;If a timestamp converts to a date in 1970, it's too &lt;em&gt;small&lt;/em&gt; for milliseconds (someone divided by 1000, or it's genuinely seconds read as ms). If it lands in 5138, it's too &lt;em&gt;big&lt;/em&gt; for seconds — a milliseconds value read as seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical fix
&lt;/h2&gt;

&lt;p&gt;Pick seconds as your wire format and be explicit about it: &lt;code&gt;Math.floor(Date.now() / 1000)&lt;/code&gt; in JS, &lt;code&gt;int(time.time())&lt;/code&gt; in Python, &lt;code&gt;time.Now().Unix()&lt;/code&gt; in Go. Never send raw &lt;code&gt;Date.now()&lt;/code&gt; to a backend that expects seconds — write the division into a named helper so nobody "fixes" it later.&lt;/p&gt;

&lt;p&gt;And when you're debugging a log line full of raw integers, a converter that handles &lt;strong&gt;auto-detection&lt;/strong&gt; saves the day: paste the value, get both local and UTC time instantly. I keep &lt;a href="https://codetoolbox.pro/tools/timestamp-converter.html" rel="noopener noreferrer"&gt;CodeToolbox's Unix Timestamp Converter&lt;/a&gt; bookmarked for exactly this — it auto-detects seconds vs milliseconds, so the 1970/5138 confusion never even starts. It runs entirely in the browser, which matters when the timestamp you're debugging belongs to a customer's private data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Got a timestamp horror story? The "5138 bug" and the "1970 bug" are the same mistake from opposite directions — drop yours in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 Cron Gotchas That Silently Break Your Jobs (and How to Fix Them)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sun, 06 Sep 2026 13:04:02 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/5-cron-gotchas-that-silently-break-your-jobs-and-how-to-fix-them-1658</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/5-cron-gotchas-that-silently-break-your-jobs-and-how-to-fix-them-1658</guid>
      <description>&lt;p&gt;Cron looks simple: five fields and a command. But some of its oldest quirks are exactly where jobs fail silently — usually at 3 AM when nobody is watching. Here are five gotchas I've hit (or watched teammates hit), with the fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Day-of-month and day-of-week are OR, not AND&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;30 2 1 * 1&lt;/code&gt; does NOT mean "the first of the month AND Monday." Cron fires the job if EITHER field matches — so this runs at 2:30 AM on the 1st of every month AND at 2:30 AM every Monday. To target a true "first Monday," keep the schedule simple and add a guard inside the command that checks the day of month is within the first 7 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cron runs in the server's timezone, not yours&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0 0 * * *&lt;/code&gt; means midnight in whatever timezone the host is set to. Docker containers default to UTC, and cloud VMs are often UTC too — so your "daily midnight" job can silently run at 8 AM your time. Check with the &lt;code&gt;date&lt;/code&gt; command in the same environment, and use &lt;code&gt;CRON_TZ&lt;/code&gt; if your cron implementation supports it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The percent sign means newline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a crontab entry, &lt;code&gt;%&lt;/code&gt; is translated to a newline. A command like &lt;code&gt;date +%Y-%m-%d&lt;/code&gt; will break or behave oddly. Escape it as &lt;code&gt;\%&lt;/code&gt; — or better, put the logic in a script file and keep the crontab line trivial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cron gives you a minimal environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cron does not source your shell profile. PATH is often just &lt;code&gt;/usr/bin:/bin&lt;/code&gt;, so anything installed via nvm, pyenv, or a project virtualenv "works in my terminal" but fails only under cron. Fix: use absolute paths in the script and export PATH at the top of the script itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Overlaps and silence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a job runs longer than its interval, cron happily starts a second instance — wrapped in &lt;code&gt;flock&lt;/code&gt; to prevent it. And if stdout isn't redirected and no mail daemon exists, error output quietly disappears. Redirect to a log file and add an external health check so a dead job actually alerts you.&lt;/p&gt;

&lt;p&gt;Gotchas like these are why I test a schedule before deploying it. When I need to decode an unfamiliar expression or build one from scratch, I use the free &lt;a href="https://codetoolbox.pro/tools/cron-generator" rel="noopener noreferrer"&gt;CodeToolbox Cron Generator&lt;/a&gt; — it validates each field and explains the schedule in plain language, and everything runs locally in your browser, so nothing gets uploaded.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>UUIDv7 vs UUIDv4: Why Time-Sortable IDs Are Winning (and When v4 Is Still Right)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:03:21 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/uuidv7-vs-uuidv4-why-time-sortable-ids-are-winning-and-when-v4-is-still-right-5h1m</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/uuidv7-vs-uuidv4-why-time-sortable-ids-are-winning-and-when-v4-is-still-right-5h1m</guid>
      <description>&lt;p&gt;If you have ever used a random UUID as a database primary key and watched writes slow down as the table grew, it was not your imagination — and the fix became an official standard back in May 2024.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with random keys&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A UUID v4 is 122 bits of pure randomness. Insert a few million rows and every new row lands at a random position in your B-tree index. The database keeps splitting pages and evicting cache lines, and your writes pay for it. On write-heavy tables this fragmentation is a real, measurable cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What UUIDv7 changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UUIDv7 keeps the familiar 128-bit shape but replaces the first 48 bits with a Unix timestamp in milliseconds. The remaining bits are still random. Because IDs generated close together are close in value, new rows append near the end of the index instead of poking holes in the middle. B-tree locality improves, page splits drop, and you get a free bonus: rows are roughly sortable by creation time, which makes "latest first" queries and cursor pagination noticeably cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adoption is already here&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.14 added &lt;code&gt;uuid.uuid7()&lt;/code&gt; to the standard library&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;uuid&lt;/code&gt; npm package ships &lt;code&gt;uuidv7()&lt;/code&gt; (v11+)&lt;/li&gt;
&lt;li&gt;Go's &lt;code&gt;google/uuid&lt;/code&gt; has &lt;code&gt;NewV7()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Postgres has the &lt;code&gt;pg_uuidv7&lt;/code&gt; extension, and built-in support keeps spreading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The trade-offs you should know&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UUIDv7 leaks timing information — the timestamp is right there in the ID. That is fine for database keys but wrong for security tokens, session IDs, or anything where you do not want to reveal when a record was created. Use v4 or a dedicated random token there.&lt;/p&gt;

&lt;p&gt;Also, "time-sortable" is best-effort, not a guarantee: if one process generates IDs in a tight loop, or a machine clock rolls back, ordering can drift. For most backends that is noise; for strict event ordering you need a sequence, not a UUID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When v4 is still the right call&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your table is read-heavy or sits under roughly ten million rows, v4 is perfectly fine — the index overhead is negligible compared with the simplicity. v4 is also what &lt;code&gt;crypto.randomUUID()&lt;/code&gt; in browsers and Node.js produces today, and it works everywhere with zero new tooling. Do not migrate an existing happy system just to chase v7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I need a UUID in the middle of debugging, I use a free client-side generator at CodeToolbox — everything runs in the browser, nothing is uploaded, and the page includes a quick v4 vs v7 vs ULID comparison if you want the full picture before choosing an ID strategy for your next project.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your JSON Won't Parse: 6 Invisible Character Traps (and How to Fix Them)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:02:29 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-json-wont-parse-6-invisible-character-traps-and-how-to-fix-them-4o77</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-json-wont-parse-6-invisible-character-traps-and-how-to-fix-them-4o77</guid>
      <description>&lt;p&gt;You've written JSON that looks perfectly fine. Braces match, keys are quoted, commas are in place. Yet &lt;code&gt;JSON.parse()&lt;/code&gt; throws &lt;code&gt;Unexpected token&lt;/code&gt; at a position that makes no sense.&lt;/p&gt;

&lt;p&gt;Sound familiar? Here are six invisible traps that break perfectly "correct-looking" JSON — and how to spot each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Trailing commas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{"a": 1, "b": 2,}&lt;/code&gt; — the comma after the last item is invalid in strict JSON (it's legal in JavaScript objects, which is why it sneaks in). Editors rarely flag it. Drop the final comma and it parses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Smart (curly) quotes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy JSON out of a word processor, email client, or chat app and the quotes come back curly: " and " instead of ". JSON only accepts the straight double quote (U+0022). Curly quotes are the #1 cause of "my JSON works in my head but not in code".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The hidden BOM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Files saved as UTF-8 with BOM start with an invisible byte sequence before the first &lt;code&gt;{&lt;/code&gt;. &lt;code&gt;JSON.parse()&lt;/code&gt; sees it as an unexpected token — the classic "error at line 1, column 1" mystery. Save as UTF-8 without BOM and it disappears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Non-breaking spaces&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A space that isn't a space. NBSP (U+00A0) copied from formatted text looks like whitespace to your eyes but is invalid JSON syntax when it hides between keys and colons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Single quotes and unquoted keys&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{'a': 1}&lt;/code&gt; and &lt;code&gt;{a: 1}&lt;/code&gt; are valid JavaScript — but not JSON. RFC 8259 requires double-quoted keys and values. Hand-written config files drift into JS-object syntax all the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Duplicate keys don't throw — they silently overwrite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{"id": 1, "id": 2}&lt;/code&gt; parses fine, but the first value is silently discarded. Different parsers disagree on which value wins, so this causes subtle, hard-to-debug data loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to debug these fast&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't eyeball it — get a precise error location. Paste the payload into a formatter that reports the exact line and column of the first syntax error (a BOM shows up as an error at position 0, a dead giveaway). A collapsible tree view also makes duplicate keys easy to spot after a successful parse.&lt;/p&gt;

&lt;p&gt;I use the free &lt;a href="https://codetoolbox.pro/tools/json-formatter.html" rel="noopener noreferrer"&gt;JSON Formatter&lt;/a&gt; on CodeToolbox for this — it's 100% client-side (your data never leaves the browser), reports exact error positions, and switches between format / minify / validate modes. No signup, no uploads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 30-second fix checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace curly quotes with straight quotes&lt;/li&gt;
&lt;li&gt;Drop trailing commas&lt;/li&gt;
&lt;li&gt;Save without BOM&lt;/li&gt;
&lt;li&gt;Double-quote every key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nine times out of ten it's one of these. Happy parsing!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Compress Images for the Web Without Losing Quality</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:08:19 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/how-to-compress-images-for-the-web-without-losing-quality-152c</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/how-to-compress-images-for-the-web-without-losing-quality-152c</guid>
      <description>&lt;p&gt;Images are the single biggest contributor to slow page loads — yet most developers only compress them once, at export time, and never think about it again. A 5MB photo that should weigh 150KB is the difference between a page that loads in 0.8s and one that crawls past 4s on mobile.&lt;/p&gt;

&lt;p&gt;Here are the compression techniques that actually matter, in order of impact:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Resize before you compress.&lt;/strong&gt;&lt;br&gt;
If your original is 4000x3000px but your layout only needs 1200px wide, every pixel beyond that is wasted bytes. Resizing first, then compressing, routinely turns a 5MB photo into under 100KB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use WebP as your output format.&lt;/strong&gt;&lt;br&gt;
WebP is typically 25-35% smaller than JPEG at the same visual quality, and every modern browser supports it. For screenshots and UI mockups, converting PNG to WebP is often an 80-90% reduction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Find the quality breakpoint, don't guess.&lt;/strong&gt;&lt;br&gt;
Start at 100% quality and slide down while previewing the result side by side. The moment you notice degradation, bump back up 5%. For most web images that lands between 70-80% — visually near-identical, 60-80% smaller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Match quality to usage.&lt;/strong&gt;&lt;br&gt;
Hero images and photography portfolios deserve ~85%. Thumbnails and icons can drop to 60% where detail matters less. One setting for everything means you're either bloating thumbnails or degrading heroes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Watch your Core Web Vitals.&lt;/strong&gt;&lt;br&gt;
Image weight is the #1 factor in Largest Contentful Paint (LCP). Cutting image size on your top pages can shave seconds off LCP on mobile — and since Google uses LCP as a ranking signal, it's a free SEO win alongside the speed gain.&lt;/p&gt;

&lt;p&gt;When I need to batch-process images quickly, I use a browser-based compressor at &lt;a href="https://codetoolbox.pro/tools/image-compressor" rel="noopener noreferrer"&gt;codetoolbox.pro/tools/image-compressor&lt;/a&gt; — it runs entirely locally via the Canvas API, so nothing gets uploaded to a server, which matters when you're compressing screenshots of internal dashboards or customer data.&lt;/p&gt;

&lt;p&gt;The rule of thumb: if a page's images are under 100KB total, you're probably fine. If a single image is over 500KB, it's costing you ranking and conversion. Compress once, deploy everywhere.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
