Retours de terrain

De vrais incidents avec le symptôme, le diagnostic et la conclusion

2026-10-17 · nginx

Why does a missing file return 200?

Debugging a case where an image was missing but the page showed no error. curl said /images/ returned 200, and the body was the homepage HTML.

The cause is the SPA fallback:

location / {
    try_files $uri $uri/ /index.html;
}

For a missing path, the first two candidates miss and it lands on /index.html — which is 200. That is correct for a single-page app, which needs it so client-side routes survive a refresh. But it also means every missing file returns 200.

Two practical consequences. First, you cannot judge whether a file exists by status code alone: a verification file I thought was deployed returned 200 for weeks while not existing. Compare content (an md5) instead. Second, when $uri/ matches a *real directory*, index handling takes over — no index.html there yields 403, not a fallback. Adding seven language directories without index.html turned /en/ and friends from 200 into 403 in one deploy.

Hard rule now: any new directory under dist must contain index.html, enforced by a build-time check.

Paste the response headers to see the real content type →

2026-10-20 · encoding

Why Chinese in my QR code scanned as garbage

After replacing a third-party QR API with a local library, English worked fine but Chinese scanned as garbage.

Encoding, obviously — so I wrapped the input in encodeURIComponent. No change. Tried two scanner apps; same garbage.

The cause was the library default: it converts a string to bytes by taking the low 8 bits of each character (latin1). Correct for ASCII, and for each Chinese character it keeps only the lowest byte of the code point — four characters became 4 bytes instead of the 12 UTF-8 requires. Wrong byte count means wrong content.

The diagnostic is one line: print the byte array length and compare with UTF-8 expectations. The fix is one line: assign the library's UTF-8 string-to-bytes function before use.

Lesson: "English works" is not "the feature works" after swapping a library. Always verify text-encoding code with non-ASCII input, and verify *content*, not merely that output exists.

Generate one with Chinese text and scan it →

2026-10-21 · auth

Signature verification failed — actually the token expired

The API started returning "invalid token". I assumed a wrong secret and re-checked it end to end; it matched. Then I tried different algorithms — HS256, RS256 — nothing.

Half an hour in, I finally decoded the token. The middle segment's exp field was 1700000000, which is 2023. The token had expired long ago.

Why the misleading error? Many libraries verify the signature first and treat expiry as a generic authentication failure, so the response looks exactly like a signature mismatch.

New habit: for any token error, decode and read exp first. It takes five seconds. Also remember exp is in seconds, not milliseconds — treating it as ms yields dates in 1970 or the far future, which is a different bug entirely.

Paste the token and read exp →

2026-10-22 · debug

Two environments "looked identical" but differed in one field

Staging was fine, production was not. I copied both responses and compared them — three passes by eye, no visible difference. Same field names, same nesting, same-looking values.

Eye comparison is inherently unreliable: two 80-line responses with identical indentation, differing only in the second array element's status going from 1 to 2 — a field that decides whether risk control runs.

A structural diff printed one line: $data.list[1].status: 1 → 2. Three seconds.

Also worth knowing: a plain text diff flags every following line when one array element is inserted, making one change look like forty. A structural diff matches by key and reports one. Rule: never text-diff JSON.

Paste both sides and get path-level differences →

2026-10-22 · payment

WeChat Pay signature kept failing over one space in the body

Integrating WeChat Pay v3, signatures always came back as invalid. Private key checked, merchant ID checked, certificate serial checked, timestamp confirmed in seconds.

The signed message is five lines: method, path, timestamp, nonce, body — each terminated by a newline, including the last one. And the body must be the exact bytes sent, not "equivalent JSON".

My code serialized an object for signing, then serialized again when sending. Between the two, the HTTP client added a space: {"a":1} became {"a": 1}. Identical semantics, different bytes, signature mismatch.

The fix is not in the signing code: build the body string once, sign that string, send that string verbatim. Also note the path must include the query string exactly as sent — do not re-sort parameters.

Paste key and body; it shows the exact signed string →

2026-10-23 · validation

Bulk ID import failed on the last digit

A customer imported an employee list and the system rejected "invalid ID format" — but the numbers looked perfectly normal in the spreadsheet: 18 digits, numeric ending, no spaces.

A quick script regex-checked 20 of them: all passed. So it was not the format, it was the checksum.

The last digit of an 18-digit Chinese ID is a check character: the first 17 digits are weighted, summed, and taken modulo 11 against a lookup string. Part of that batch had a lowercase x instead of X; another part had been stored as a float by the spreadsheet, silently altering the trailing digit.

Validating an ID needs three checks: format (18 chars, last is a digit or X), the check character, and whether the birth date actually exists (no February 30). Regex alone is not validation — an invented 18-digit number almost always passes.

Validate ID cards, USCC and bank cards in one place →