---
title: "Why Request.Form Is Empty: FormData vs URLSearchParams in Classic ASP AJAX"
canonical: https://dxdev.com/blog/formdata-vs-urlsearchparams-classic-asp-ajax/
datePublished: 2026-05-10
---
# Why Request.Form Is Empty: FormData vs URLSearchParams in Classic ASP AJAX

The POST fired. The network tab showed a clean 200, a request body with all my fields in it, every value right there in plain sight. And `Request.Form` on the ASP side came back empty. No exception, no warning in the log, no 500. Just an empty collection where my form fields should have been.

If you maintain a Classic ASP backend behind any kind of modern JavaScript frontend, you will hit this, and you will lose an hour to it before you figure out the cause. It is not your data. It is not your endpoint. It is the content type.

## The setup that breaks

I ran into this on a walkthrough overlay feature in our codebase for the sports SaaS that has been running on Windows, IIS, and Classic ASP for two decades. The frontend was modern JavaScript. The AJAX call looked like the textbook example everyone copies from MDN:

```javascript
const data = new FormData();
data.append("step", currentStep);
data.append("userId", userId);

fetch("/your-endpoint.asp", {
  method: "POST",
  body: data
});
```

This works perfectly against a Node endpoint, a PHP script, a Rails action, anything written this decade. `FormData` is the obvious, modern way to build a request body. The browser handles the encoding for you. You do not set a `Content-Type` header, because the browser sets it for you, with the correct multipart boundary.

That last sentence is the whole problem.

## What FormData actually sends

When you pass a `FormData` object as a `fetch` body, the browser serializes it as `multipart/form-data` and sets a header like:

```
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
```

The request body is then a multipart document, with each field wrapped in boundary markers:

```
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="step"

3
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="userId"

8842
------WebKitFormBoundaryABC123--
```

This is a completely valid HTTP request. It is also exactly how a browser submits a `<form>` with a file input. Modern server frameworks parse multipart bodies natively, pull out each part, and hand you a tidy key-value map. That is why it looks like it should just work.

## What Request.Form actually parses

Classic ASP's `Request.Form` collection parses one thing: `application/x-www-form-urlencoded`. That is the old-school flat encoding, the `key=value&key2=value2` string you get from a plain HTML form with no file upload. ASP was written for that world, and `Request.Form` never learned anything newer.

When ASP sees `Content-Type: multipart/form-data`, it does not parse the body into the `Form` collection. It does not throw. It does not log. It just leaves `Request.Form` empty, because as far as that collection is concerned, there was no urlencoded form data to read. The raw multipart bytes are still sitting there in the request stream, untouched, reachable only through `Request.BinaryRead`. But `Request.Form("step")` returns an empty string, and `Request.Form.Count` is zero.

So nothing errors. The browser sent a valid request. ASP received it fine. The two sides simply do not agree on what a request body looks like, and the disagreement is silent.

This is what makes it so hard to diagnose. Every instinct says to look at the data, the field names, the endpoint path, the session, the auth. The network tab actively misleads you because the request body is right there and looks correct. The one thing the network tab does not flag is that the receiving language predates the encoding the browser chose.

## The fix is one object

Swap `FormData` for `URLSearchParams`:

```javascript
const data = new URLSearchParams();
data.append("step", currentStep);
data.append("userId", userId);

fetch("/your-endpoint.asp", {
  method: "POST",
  body: data
});
```

The API is nearly identical, same `.append()` calls, same shape. But when you pass a `URLSearchParams` object as a `fetch` body, the browser serializes it as `application/x-www-form-urlencoded` and sets:

```
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
```

That is the exact content type `Request.Form` has understood since the mid-1990s. The body becomes `step=3&userId=8842`, ASP parses it, and `Request.Form("step")` returns `3`. No ASP changes. No multipart parser to write. No `BinaryRead` plumbing. You change one constructor on the client and the legacy backend is happy.

In our case the actual fix was about seventeen minutes, and most of that was finding it, not writing it. The diff was one line.

## If you would rather not touch the client

Sometimes you do not own the frontend, or it is sending multipart for a reason (an actual file upload mixed in with text fields). You have two server-side options, neither as clean as the one-line client fix:

- Set the header yourself when you build the request and keep it urlencoded. If you control the `fetch` call but want to be explicit, build a urlencoded string and set `Content-Type: application/x-www-form-urlencoded` by hand. The browser will respect a header you set, and `Request.Form` will parse it.
- Read the raw body with `Request.BinaryRead` and parse the multipart yourself. This is real work, you have to walk the boundary markers and decode each part, and it is only worth it when you genuinely need file uploads through Classic ASP. For plain text fields it is never the right trade.

When the fields are plain text, `URLSearchParams` is the answer. The trap is most likely to bite when someone modernizes a frontend on top of a legacy backend, which is exactly the situation a lot of long-lived ASP apps are in right now. You upgrade the JavaScript, copy a `FormData` snippet from current docs, and quietly break every POST to the old server without a single thing turning red.

## The takeaway

When a perfect-looking POST yields an empty `Request.Form` on a Classic ASP endpoint, stop debugging your data and check the request's Content-Type. If it says `multipart/form-data`, that is your bug. ASP only parses `application/x-www-form-urlencoded`. Swap `FormData` for `URLSearchParams` on the client and the fields come back. One object, one line, and the silent empty collection turns into the data you expected all along.

## Related

- [Response.Redirect Blew Up the Error Page. Server.Transfer Didn't.](response-redirect-vs-server-transfer-yellow-box): another ASP behavior that silently diverges from modern expectations
- [Classic ASP doesn't care that it's a comment: the script-delimiter collision](asp-delimiter-inside-comment-broke-parser): parser behavior that surprises anyone coming from modern server frameworks
- [The Datetime That SQL Server Can't Read Back: Keep the Copy In the Database](datetime-sql-server-cant-read-back-keep-it-in-sql): another silent server-side mismatch between what you send and what gets stored
- [Edit a label, get &amp;quot; back: the HTML-entity round-trip bug](html-entity-double-encode-edit-form-boundary): a different encode/decode boundary that silently corrupts data on round-trip
- [The classic-ASP bug that printed the literal word "undefined" instead of your IP](the-asp-bug-that-printed-undefined-to-every-visitor): silent Classic ASP output failure with no error thrown
