Reading the body of a request in Vercel’s Edge middleware

Jack Oddy
3 min readJun 19, 2023

--

edit: turns out the incoming request body was actually encoded form-data which is why I had so much trouble! 😂 I’ll leave this post up incase it’s useful to someone — but try request.json() and request.formData() first, or at least inspect the Content-Type header!

It should be simple right? Years of frameworks abstracting away this step will likely lead many to scratching their heads when it comes to reading the body of a request in an edge function. There’s nothing surprising here really, this is probably more of a ‘how to read a ReadableStream in node’.

Of course we’re talking about Vercel’s edge runtime, not node. but for this post they may as well be the same thing

For the sake of this example let’s say we want to verify the signature header — for that we’ll need to extract the body. We won’t go into signature verification — but if you’re interested, I also wrote about verifying a request Slack in edge middleware:

Let’s start with a middleware edge function and go from there. Here is an edge function that returns an error response when the request signature cannot be verified.

import { next } from '@vercel/edge'

async function middleware(request) {
const signature = request.headers.get("X-Signature")
const body = await extractBody(request)

if (await isSigned(signature, body) return next() // allow request to proceed

return new Response('unauthorised', { status: 403 })
}

Now we have a context to work within, let’s jump into this function called extractBody

A few things need to happen in order to extract the data from request.body it is a ReadableStream — which you can read about here. To read a stream you need to get a StreamReader which locks the stream until it is released. While it is locked to the reader the data can be streamed/read and this is just what we need to do.

async function extractBody(request) {
const reader = request.body.getReader();
}

Now a reader has been locked onto the stream, it needs to be read. Streams work by reading parts of data and doing something with that part, similar to iteration, until you’re done. In JS this is commonly achieved with a while(true){ ... loop. Let’s see what that looks like:

  while (true) {
const { done, value } = await reader.read();
if (done) break;
}

Calling read() on a reader returns an object with two values { done, value } — done being true once the end of the stream has been processed, value being the part of the stream that has been read. So now the value is available, what do we do with it? In this case, it needs to be decoded before being appended to a string to build the value of the body over time:

async function extractBody(request) {  
const dec = new TextDecoder();
const reader = request.body.getReader();
let body = ""

while (true) {
const { done, value } = await reader.read();
if (done) return body;

body = body + dec.decode(value)
}
}

Note: this will return the raw value of the body — you may be expecting JSON, but it will need to be processed further for that. For the sake of this example — signature verification, the raw version was all that’s needed

This may not be earth shattering, but I do suspect that this could trip up quite a few engineers coming from express or next.js, who will be used to a preprocessed body property to a request. I hope you found this useful!

--

--