The upload kept failing, but only some of the time. The fault was ?action=, a querystring key that the page’s main form handler and the AJAX upload handler had each claimed independently. When a form post and an image upload arrived close together, IIS resolved the ambiguity against the upload.
This was one long day inside the Create Event flow of an older web application. It started as a styling pass on the event creation page and turned into an archaeology dig once the odd failures began surfacing.
The surface was fine and the contract was broken
The ?action=create handler processes form submissions. The ?action=upload handler processes image uploads from the same page. In a modern app with real routing, a framework would catch that conflict at startup. Here, Request.QueryString("action") checks are spread across the same file, so nothing complains. The collision shows up only when a form post and an upload race on the same page. That is why it stayed intermittent and looked like an upload bug.
Claude chased the symptom competently. It went through the logs, patched the upload handler, and grepped the application files for anything else that touched ?action=. On a first read its pass looked clean, and the patch was wrong, because the handler was never the broken part.
What it could not surface was that ?action= was a shared namespace two layers had entered without knowing about each other. That fact is in no comment and no test. It lives in the implicit contract between two parts of a file that grew incrementally over a long time. The only way to find it is to hold the question “are there other callers” in your head while reading code that never asked it of itself.
The page-level handler was the one that had to move. Renaming its parameter from ?action=create to ?view=create freed action for the upload handler alone, and the intermittent failures stopped.
The filename was a path separator
Files were saving as orgeventHeader.jpg instead of org/eventHeader.jpg. A missing slash in the path construction code let the directory prefix collapse into the filename. The file landed in the wrong place under a name that looked like a corrupted string.
The fix was one character. Finding it took longer than it should have, because we were reading the filename handling code instead of the spot where the directory and filename strings get joined. In legacy code the bug shows up at the last place that touched the output, while the fault sits upstream in whatever assembled the wrong input. In a large file those two places are rarely adjacent.
The parent context that wouldn’t let go
The creation function that provisions a new event inside the production app had, at some point, had its parent-context initialization inlined into the creation flow, and that inlining broke. After the function ran, the redirect sent staff to the parent org’s admin view instead of the newly created entity’s view. The new record existed and was correctly formed. The redirect was reading from a context that still pointed at the parent.
Two faults compounded. The parent-org context was leaking into the new entity’s initial setup, and the post-create redirect read from that polluted context. Fixing only the redirect would have given us a system that worked most of the time. In a legacy codebase that is often harder to diagnose than one that fails clearly and consistently.
Local dev was lying about /photos/
Images serve from the application root on local dev and from a CDN on production. The /photos/ path was hardcoded in enough places that local tests passed while production served broken image references. Environment-aware path resolution is not a new idea. In an old codebase with no central config layer, though, it turns into a manual grep pass every time someone touches image handling. We have fixed this specific class of problem in this codebase before, and the fix never fully sticks because nothing enforces it.
What the agent could and couldn’t hold
Claude was useful for velocity. It grepped across the codebase, traced the creation function’s call chain, surfaced which files referenced ?action=, and drafted the environment-aware path logic for /photos/. An agent that stays oriented across a large legacy codebase, and doesn’t slow down on iteration nine of a nearly identical test case, is a real asset when the volume of material to read is the main friction.
What Claude couldn’t supply was the system-level invariant. The premise that ?action= is a shared namespace isn’t in a comment, a test, or any document the agent can read. It is a contract implied by the way the system grew. Violating it produces a symptom that looks like an upload bug, because that is where the failure becomes observable. The agent optimizes toward the stated symptom. You have to carry the unstated contract.
Legacy debugging is contract archaeology. The upload fix was a rename that vacated a namespace two layers had shared without knowing it, and it was made outside the upload handler. So when a legacy form fails intermittently, ask whether two parts of the application share a parameter or key that nobody documented. AI assistance speeds up the reading and searching, but someone still has to ask who else uses the same key.