📰 Latest Articles & Guides

My webhook catcher logged the same event six times — the sender's retry clock was shorter than my handler

My webhook catcher logged the same event six times — the sender's retry clock was shorter than my handler
I built an ephemeral webhook catcher because testing receivers is annoying: you need a public HTTPS URL, a running app, and logs, when all you actually want is to see the raw POST a sender makes. The catcher hands you a throwaway URL, accepts any POST for 24 hours, and lets you read the stored events back. Then I dogfooded it and found my own bug. A sender script I'd written posted a payload; my handler parsed the body, stored the JSON, and only then replied. Under a slow parse the round trip took longer than the sender's HTTP timeout. The sender's client gave up, treated it as a failed delivery, and retried. My "one payload per hook" test came back with six copies of the same event, each with a different arrival timestamp. Two lessons, both now baked into how I operate it: A receiver's latency budget is set by the caller. I was doing real work before responding. The fix is boring and old: enqueue, return 2xx in milliseconds, record asynchronously. Catchi...

My XLSX converter shipped a due date as 45852 — Excel dates are serial numbers, not strings

My XLSX converter shipped a due date as 45852 — Excel dates are serial numbers, not strings
I spotted this while testing a client's invoice pipeline: a spreadsheet whose "Due date" column displayed 14/07/2025 in Excel. My converter's Markdown output listed 45852 . Not a rendering bug — that's literally what the file stores. The stored value and the displayed value in a spreadsheet are two different things. Excel keeps dates as serial numbers: days elapsed since 1899-12-30. The odd anchor is a relic of Lotus 1-2-3, which believed 1900 was a leap year, so Excel pretends 1900-02-29 exists and serial 60 points at a date that never happened. The friendly dd/mm/yyyy you see lives in styles.xml as a number-format code attached to the cell's style — the sheet data just holds the raw float. Percentages are the same trap: 0.175 stored, "17.5%" displayed. Even booleans are stored as 1 and 0. So an agent reading my output had no way to know 45852 was a deadline. One downstream job happily treated it as a quantity and scheduled a reorder around it....

My QR PNGs looked sharp and refused to scan — pixels per module, not image size, is what matters

My QR PNGs looked sharp and refused to scan — pixels per module, not image size, is what matters
For months my QR generator's default PNG output was a flat 256px, and it worked — because everyone encoded short URLs. A typical short link lands in version 2–3 of the QR spec, roughly 25–29 modules across, so 256px gave ~8–10px per module and every phone scanner read it instantly. Then a user encoded a marketing link with tracking params — around 180 characters. The encoder picked a version 8 symbol: 49×49 modules. At 256px that's barely 5px per module, and my rasterizer antialiased the edges, so adjacent modules bled into each other. The image still looked like a perfectly normal QR code. It just wouldn't scan. Desktop scanners mostly managed; my phone failed about half of attempts at a normal distance. The lesson I'd missed: scanability is a function of pixels per module, not the image's nominal size. A 256px version 2 code and a 256px version 8 code are not the same product. On top of that, decoders expect a quiet zone — four modules of white margin on ever...

My git changelog listed a feature and its own revert in the same release — commit history isn't release contents

My git changelog listed a feature and its own revert in the same release — commit history isn't release contents
I built a git changelog generator because I was tired of LLM-written release notes inventing features. Mine is deterministic: it diffs two commits in a repo and emits a structured changelog. No model touches the history. The first real repo I ran it on humbled me: v1.3 to v1.4, 214 commits. The output read like a phone book. I counted by hand: 96 of the 214 were merge commits — Merge pull request #… , Merge branch 'release' into main . In a squash-merge repo those carry zero information; the squashed commit already has the PR title and description. git log --no-merges went into the pipeline and the list dropped to 118. One caveat: an "evil merge" can introduce changes that exist in no parent, so I only suppress merges whose message matches a recognizable template and keep the oddballs visible. Then a user did the real damage. Their v2.1 changelog listed "Added CSV export" and, further down, "Removed CSV export". Both commits were real, both in ...

My HTML-to-Markdown output passed every test I wrote — then flattened in a strict parser

My HTML-to-Markdown output passed every test I wrote — then flattened in a strict parser
An agent piped a wiki export through my HTML-to-Markdown converter into a RAG pipeline and reported that hierarchy was disappearing. Every nested task list came out flat: sub-items rendered as siblings of their parents. Sections got chunked together that had nothing to do with each other, and retrieval started returning paragraphs attributed to the wrong topic. Here's the part that stung: the Markdown looked perfect in my own renderer. That clause was the entire bug. My converter indented nested list items by two spaces. Two-space indentation is accepted by lenient parsers and most browser previews, but strict CommonMark requires indenting by the marker width — four spaces under a - bullet, three under a 1. ordered item. Feed two-space output into a compliant parser and the nesting silently collapses. No error, no warning, just a flat list wearing a nested list's clothes. My test suite only ever checked output against one parser: the lenient one. Round-trip validation with...

Canvas doesn't wrap text — my OG image generator painted titles off the 1200px edge

⚡ INAPP
When I built an OG image generator — POST a title and description, get back a 1200x630 social-card PNG — the first render looked perfect. The second, a ~90-character title about database indexing, ran clean off the right edge and clipped mid-word. The root cause was embarrassing: Canvas's fillText() doesn't wrap. Ever. It draws one line and happily paints 3,000px of text onto a 1,200px image. Browsers wrap HTML; canvas is a paint API, not a layout engine. If you want wrapped text, you build the line-breaking logic yourself. What ended up being the real core of the product: 1. Measure, never estimate. Every word's width comes from ctx.measureText(). My first heuristic (average character width × length) broke instantly on ALL-CAPS titles and wide glyphs. 2. Greedy line packing against the safe area: 1200px minus 64px padding per side = 1072px usable. Append a word; overflow means a new line. 3. Cap at three lines with an ellipsis — but only append "…" if the last...

DOCX, PPTX, XLSX and EPUB all start with the same magic bytes — auto-detect means asking the ZIP what it is

DOCX, PPTX, XLSX and EPUB all start with the same magic bytes — auto-detect means asking the ZIP what it is
When I added auto-detect to my universal document converter, I assumed file extensions and Content-Type headers would carry the weight. Both lied to me within the first week. A contract arrived named contract.pdf . The extension said PDF, the upstream header said application/pdf. My PDF parser choked on byte zero: the file was actually a DOCX. Someone had renamed an export, and a proxy in the delivery chain had stamped the header based on the filename. Nothing on the outside matched what was inside. Magic bytes seemed like the obvious fix, and PDFs declare %PDF , so that part worked immediately. Then an uncomfortable fact surfaced: DOCX, PPTX, XLSX, and EPUB are all ZIP archives. They share the exact same PK\x03\x04 signature — one magic number, four formats. The disambiguation lives inside the container: EPUB is required to store a file literally named mimetype as the first entry, uncompressed, containing application/epub+zip . Two reads and you're done. Office formats carr...

Mid-stream failover made my chat API answer the same prompt twice — switch models before the first token or not at all

Mid-stream failover made my chat API answer the same prompt twice — switch models before the first token or not at all
A client's transcript came in looking like a slip of the tongue from a language model: three cut-off sentences about caching, then the exact same question answered again, in a slightly different voice, spliced together as one continuous message. The culprit was failover logic in my chat router. I run an OpenAI-compatible /v1/chat/completions endpoint with 50+ models behind one integration ( https://x402.freeq.one/tools/llm_chat.html ), and the eco tier picks the cheapest healthy provider for each request. One night an upstream started handing out 429 s — but only after accepting the connection and streaming about forty tokens. My router treated 429 as retryable, re-dispatched the prompt to the next provider on the list, and appended the new stream to the old one. The client's SDK happily glued both halves into a single message. The fix is a rule I now enforce in the stream state machine: failover is only legal in the zero tokens sent state. Connection refused, a 401 or ...

My EAN-13 barcodes rendered flawlessly and every retail scanner rejected them — the check digit isn't decoration

⚡ INAPP
First real complaint about my barcode tool came from someone printing product labels for a shop. The EAN-13s came out visually perfect — crisp bars, correct proportions, human-readable digits underneath. Her handheld scanner buzzed on every single one and refused to read. Root cause: I was encoding whatever digits the caller sent, treating the 13th as just another digit. I assumed valid EAN-13 input. She'd copied a code off a wholesaler sheet where the final digit didn't match what the other twelve implied — and EAN readers recompute that last digit on every scan. No match, no read. No error message, no log. Just a buzz. The most frustrating failure mode there is: silent rejection at the consumer's hardware, invisible at my end. That 13th digit isn't data — it's verification. Take the first 12 digits, weight them alternately 1 and 3 starting from the right, sum the products, and the check digit is whatever makes the total a multiple of 10. Every retail scanner run...

Word doesn't store list numbers in the text — my DOCX converter printed clauses as bare paragraphs

Word doesn't store list numbers in the text — my DOCX converter printed clauses as bare paragraphs
A user fed a 40-page contract into my DOCX converter and complained that every clause came out as a plain paragraph — no bullets, no numbers, just walls of text. The original Word file had nested numbered clauses (1.2.3 style) and my output flattened all of it. I assumed I was reading malformed XML. I wasn't. OOXML simply doesn't store list markers in the document text. A numbered paragraph looks like: <w:p><w:pPr><w:numPr><w:ilvl w:val="1"/><w:numId w:val="4"/></w:numPr></w:pPr><w:t>Deliverables</w:t></w:p> There's no "1." anywhere in the file. Word generates the marker at render time by looking up numId 4 in a separate part of the zip archive, numbering.xml , which maps abstract list definitions to formats — decimal, lowerLetter, romanNumeral, bullet — each with its own start value and indent level. My first version read only document.xml, so every list item printed as bare text....