Getting POST parameters via request.formData() prevents forwarding the original request?


#1

Hey there,

I’m writing a worker that takes in client API requests, inspects them to see if the response is cacheable and forwards the original request to the origin if it is not.

I need to gather up arguments from all the different ways our API endpoints allow the client to pass parameters (GET, POST, in the top level of the payload, etc.). When I call “await request.formData()” to get the contents of the POST body, I get the arguments passed by the client, canonize the request format and create a new request to our edge-cacheable URL location.

The problem comes when my worker logic determines that the API call should actually not be cached and instead the original request should simply be forwarded to the origin ( “return fetch( request );” ). When I try to forward the original request I get this fatal error:

“Uncaught (in promise) TypeError: This readable stream is currently locked to a reader.”

I tracked it down to the “await request.formData()” call. I tried a lot of stuff (playing around with request.body.getReader() etc.) but haven’t been able to figure anything out.

I suppose I could always manually reconstruct a new request that looks exactly like the one passed in to my worker, but I would expect to be able to just forward the original request like I have been before I needed to get the POST parameters. If feel like there is probably just something I am doing wrong or missing.


#2

Hi Spencer,

Indeed, you can only read the body of a Request once, since it is read in a streaming way. This is a property of the Service Workers API, which is designed to avoid buffering.

What you can do here is construct a new request using the form data you parsed:

let formData = await request.formData()
let newRequest = new Request(request, { body: formData })

Since this new request is constructed from an in-memory object (not a stream), it can be sent any number of times.


#3

I thought I might have to do something like that. Still learning about service workers and wanted to make sure that I’m not missing something.

I tried constructing a new request like you demonstrated above but the request that is ultimately sent to the server looks much different than the original. Instead there are long identifiers prefixed with “–” and stuff like “Content-Disposition:_form-data;_name” which my API backend unfortunately doesn’t know how to handle.


#4

It seems like I can manually reconstruct the POST body from extracted key - value pairs in the formData, and when constructing the new request it sends as expected. I don’t think this will handle the case where somebody passes a unstructured blob in the request body though.


#5

Hi @spencer.nielsen,

So, it seems this is an oddity in the specs.

FormData is intended to represent data of type “multipart/form-data”, whereas URLSearchParams is meant to represent data encoded as application/x-www-form-urlencoded. So when you pass a FormData as a body, it gets encoded in the multipart format, which is what you’re seeing.

The weird thing is that when you call request.formData() to read a request, it accepts either format, effectively coercing application/x-www-form-urlencoded into FormData. Meanwhile, there is no request.urlSearchParams() or anything like it.

What you can do instead is read your request into a plain String, and then parse URLSearchParams from that:

let body = await request.text()
let formData = new URLSearchParams(body)
let newRequest = new Request(request, { body })