金曜日の修正: 修正されなかった修正
原題: Friday Fixes: The Fix That Wasn't
分析結果
- カテゴリ
- AI
- 重要度
- 65
- トレンドスコア
- 27
- 要約
- この記事では、期待された修正が実際には行われなかったことについて述べています。問題の根本的な原因や、修正がなぜ実現しなかったのかを分析し、今後の改善策や教訓を考察しています。読者にとって、問題解決のプロセスやその結果を理解する手助けとなる内容です。
- キーワード
Three bugs this month. All three looked fixed before they broke. The date was quoted in 51 out of 52 posts. The model was pinned to a specific version. The upload feature had been working in production for weeks. Each one passed the obvious checks and failed somewhere else. That's the theme for this Friday Fixes: the fix that wasn't. Not bugs that went unnoticed, but bugs where a defense existed and the failure found its way around it. 1. The Unquoted Date, Part Two If this one sounds familiar, it should. I wrote an entire Friday Fixes post about this exact bug class five weeks ago. An unquoted YAML date. gray-matter parsing it as a Date object instead of a string. A crash downstream. Last time it took down /admin/drafts . The fix hardened formatDate() to coerce Date objects before calling .includes() . I verified it. I shipped it. I wrote 2,000 words about it. I moved on. This time it took down the homepage. The symptom: vibescoder.dev loaded for a split second, then flashed to Chrome's "This page couldn't load" screen. Every browser, every profile, every device. The site was completely dead to visitors. The twist: curl returned HTTP 200 with ~900KB of fully rendered HTML. The server was fine. The crash was happening during React hydration in the browser, invisible to any server-side test. The cause: A new post had date: 2026-06-19 in its frontmatter. No quotes. gray-matter parsed it as a Date object. In posts.ts , the code does const meta = data as PostMeta and then spreads ...meta into the return value. The as PostMeta cast told TypeScript the date was a string . At runtime, it was a Date . That Date object flowed through the server component, through the RSC serialization boundary, and into PostListWithFilters , a "use client" component. React couldn't hydrate it. No global-error.tsx existed to catch the crash. Dead page. Why the May fix didn't prevent this: Because the May fix was in the wrong layer. It hardened formatDate() , the function that happened to crash that time. It never hardened posts.ts , the layer where the Date object enters the system. The Date object simply found a different path out. The false start: The first fix attempt added meta.date instanceof Date to coerce the value. TypeScript rejected it: Type error: The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter. The same as PostMeta cast that hid the runtime bug also blocked the fix. TypeScript believed meta.date was a string , so it wouldn't let me check if it was a Date . The fix was to check data.date (the raw gray-matter output, typed as any ) instead of meta.date (typed as string ): function normalizeDate ( raw : unknown ): string { if ( raw instanceof Date ) return raw . toISOString (). split ( " T " )[ 0 ]; return String ( raw ); } Applied in all four functions that return post data. Also added a global-error.tsx so future hydration crashes show a reload button instead of a dead page. What it cost: ~25 minutes of downtime on the public site. Three commits across two repos, including the TypeScript false start. One embarrassing realization that I'd written a blog post about the bug and it happened again anyway. 2. The Model That Quietly Expired The blog has a voice dictation flow: record a transcript, click "Generate Post," get a draft. On June 18, clicking Generate returned a red "Generation failed" banner. No useful error detail. The cause: The generation pipeline called the Anthropic API with model: "claude-sonnet-4-20250514" . That model hit end-of-life on June 15. The API started rejecting requests three days before anyone noticed. The clue was in the SDK itself: // @anthropic-ai/sdk DEPRECATED_MODELS ' claude-sonnet-4-20250514 ' : ' June 15th, 2026 ' , The fix: One line. -model: "claude-sonnet-4-20250514", +model: "claude-sonnet-4-6", Merged as PR #17. Generation worked immediately after Vercel deployed. Why it took three days: Two compounding failures: First, there's no deprecation warning from the Anthropic API. The model works on June 14. It doesn't work on June 15. No sunset header, no grace period, no degraded response with a warning. Just errors. Second, the catch block swallowed the error. The route handler logged console.error("Generation error:", error) to the server, but returned { error: "Blog generation failed" } to the frontend. The actual Anthropic error message, which almost certainly said something about the model being retired, was buried in Vercel's server-side logs. The user-facing error was a generic string that could mean anything. A comment like // EOL: June 15, 2026 next to the model string would have made this a 30-second fix. Surfacing the API error to the frontend would have made it self-diagnosing. Neither existed. 3. Seven Commits for Three Lines The Vacation Hub, a trip planning side project, has a photo gallery. Upload photos from your phone, they land in Vercel Blob Storage. It worked perfectly on the original deployment. After a security hardening commit that added CSP headers, photo uploads broke. Click upload, progress bar hits ~20%, hang forever. The agent spent seven commits fixing this. The actual fix was three lines. What went wrong: The security commit added a Content-Security-Policy header with connect-src 'self' https://*.public.blob.vercel-storage.com . The Vercel Blob SDK's client-side upload() makes a PUT to https://vercel.com/api/blob . That domain wasn't in connect-src . The browser silently blocked the request. But here's why seven commits: there were three independent bugs stacked on top of each other, and fixing any one of them didn't resolve the issue. CSP connect-src missing https://vercel.com caused the hang. The browser blocked the PUT, no error surfaced, the upload promise never resolved. Empty onUploadCompleted callback contributed to the hang. The SDK registered a webhook URL that Vercel would POST to after upload. The empty handler existed, so the SDK set it up, but the callback could silently fail. No multipart: true on the upload calls. Vercel Blob's single PUT has a 4.5MB limit. Modern phone photos regularly exceed that. Without multipart chunking, large files returned 413. But you'd never see the 413 if the request never got past CSP. Each bug masked the next. Fix the CSP and uploads still hang (callback). Remove the callback and large photos 413 (no multipart). The agent tried each fix in isolation, concluded each one was wrong, and at one point rewrote the entire upload flow to server-side FormData, which introduced its own size limit problems. The breakthrough came when I asked a simple question: "The original deployment worked. What changed?" A targeted git show on the security commit would have found the CSP addition in minutes. Instead, the agent read the current code looking for problems rather than diffing backward from the last known working state. The actual fix: -connect-src 'self' https://*.public.blob.vercel-storage.com +connect-src 'self' https://*.public.blob.vercel-storage.com https://vercel.com Plus multipart: true on both upload() calls and removing the empty callbacks. Three lines across two files. What Connects Them All three bugs involve a defense that felt complete but wasn't. The date coercion in formatDate() protected the function that crashed in May. It didn't protect the serialization boundary that crashed in June. The model was pinned, but nobody tracked when the pin expired. The security headers were added, but the SDK's upload domain wasn't in the allowlist, and the error was swallowed so thoroughly that seven commits went by before the agent found all three stacked failures. Each fix addressed the symptom it could see. None of them addressed the layer where the problem actually lived. The date needed to be coerced at the parsing boundary, not at the formatting boundary. The model needed a deprecation calendar, not just a version string. The security commit needed a full audit of outbound domains, not just the ones the developer remembered. This is a pattern I keep seeing when building with agents. You're working across dozens of sessions. The agent that added the CSP header wasn't the agent that debugged the upload failure. The agent that hardened formatDate wasn't the agent that needed to harden posts.ts . Each session is competent in isolation. The gaps live in the seams between sessions, where one agent's fix becomes another agent's assumption. The shared context between those sessions is you. The human collaborator is the one who remembers that this date bug happened before, that CSP headers can block SDK calls, that model strings have expiration dates. Agents don't carry that across sessions unless you build it into their context explicitly, with skills, with rules files, with the kind of institutional memory that a solo developer usually keeps in their head. That means bugs accumulate. Not dramatically, not in ways that show up in code review, but in the quiet gaps between what one session assumed and what the next session inherited. An unquoted date here. A hardcoded model string there. A CSP header that covers the domains you thought about but not the one the SDK uses internally. Each one is fine until it isn't. The honest response to this is not to stop using agents. It's to be vigilant. Scan for bugs and vulnerabilities constantly. Accept that some will surface in production despite your best efforts. Build error boundaries. Surface errors instead of swallowing them. Add the global-error.tsx before you need it. For a personal blog like this one, the risk is worth the reward. Agents push to production, release velocity stays high, and when something breaks, the blast radius is my own site. I can tolerate 25 minutes of homepage downtime in exchange for shipping a post every other day with a full admin toolchain that an agent built. That calculus changes the moment customers or revenue depend on what you're building. If this were a SaaS product, the unquoted date crash would