---
title: "The bug wasn't the upload, it was the URL contract"
canonical: https://dxdev.com/blog/2026-06-27_the-bug-wasn-t-the-upload-it-was-the-url-contract/
datePublished: 2026-04-16
---
The upload was failing intermittently, about one time in four, which made it look like flaky network garbage instead of a real bug. This was one day inside the Create Event flow on a tournament platform running ASP-classic that is closing in on two decades old. What started as a styling pass on the event-creation page turned into an afternoon of `git blame` and `Server.MapPath`.

## a shared key in the querystring

The create-event page loaded with `?action=create` in its querystring. A few clicks later, when the image-upload widget on that same page fired its own AJAX request, it appended its own `?action=upload`. Two handlers, written months apart, had each claimed the same key. IIS received both, the server-side switch on `Request.QueryString("action")` matched neither cleanly, and the upload silently no-opped. Nothing in a comment or a test said that `action` was a namespace shared between a page-level parameter and an AJAX parameter, because the two handlers were never written with each other in mind.

The fix renamed the page-level parameter off of `action` entirely:

```
- var forceCreate = (String(Request.QueryString("action")).toLowerCase() === "create");
+ // Use ?view=create (NOT ?action=create): `action` collides with the AJAX action
+ // param appended by Submit_ImageUploadTool, causing IIS to receive both values
+ // and silently failing the upload.
+ var forceCreate = (String(Request.QueryString("view")).toLowerCase() === "create");
```

Four lines. The upload code itself was fine the whole time.

## a stripped separator

Once uploads landed, some of them landed at the wrong path. `Server.MapPath` strips a trailing backslash, so `destDir + "eventHeader.jpg"`, where `destDir` was built assuming the slash survived, produced a string with nothing between the folder name and the filename. The fix was one character: build `destPath` as `destDir + "\\eventHeader.jpg"` instead of trusting the trailing slash to still be there after `MapPath` touched it.

## the copy that ran in the wrong context

There was a second, independent copy of the same image-copy logic, inlined directly into the account-creation handler (`Home.asp`, `MakeHQ`) as a shortcut. That handler mutates its own page context over to the new league mid-call, so by the time the inline copy block ran, the parent org's upload folder could no longer be located from that context. It failed silently and non-fatally, wrapped in a bare `try/catch`, and events kept getting created with no header image and no error anywhere to explain why. The fix deleted the inline duplicate, sixteen lines, nothing added, and routed back through the original AJAX handler, which runs in the parent's session before the account switch happens. The correct version of this already existed elsewhere in the codebase. It just wasn't the one being called.

The post-create redirect had its own version of the same problem. It built the destination URL with a `pi=1` sentinel and the parent's `uo=` parameter attached, which was enough to make the session resolver land staff back in the general staff admin view instead of the league they had just created. Swapping the hand-built URL for the app's own `AccountChange(newUN, {pageName:'bracketmanager'})` helper fixed it, because that helper already knew how to switch session context correctly.

## local dev was answering a different question than prod

Last one: the image preview was built against a hardcoded CDN media domain in two files. On production that is correct, the CDN serves the file. On a local dev clone, uploads land on the local filesystem, so the hardcoded CDN path 404s, and the code's own fallback quietly swaps in a sport-theme default image that looks intentional. Nothing in a local test run tells you it's wrong. The fix branches on hostname, `/^l\d*$/i.test(window.location.hostname)`, to decide whether to prefix the image path with the CDN domain or leave it relative.

## what Claude Code held and what it didn't

Claude Code was fast at the mechanical parts of this: grepping every ASP file under the tournament folder for `?action=`, tracing the call chain through `Create_Event_Save_Image`, drafting the hostname check for the local-versus-CDN split. What it couldn't supply on its own was the fact that `action` was a shared namespace in the first place. That fact lives in the relationship between two files that were never edited side by side, not inside either file on its own. Reading a diff for what isn't there, and isn't going to be flagged by a grep, is still the part a person has to do.

One commit closed all five of these on 2026-04-16: the querystring rename, the MapPath separator, the deleted duplicate copy block in `Home.asp`, the `AccountChange` swap, and the local-versus-CDN branch. Ten files touched, 62 lines added, 47 removed. Four of those five had never been filed as their own ticket. Three of them looked, from the outside, like the same complaint as the one that was: the upload doesn't work. The fourth, the account-creation redirect, looked like something else entirely, staff landing on the wrong screen after creating an event, and it got fixed in the same pass anyway.
