I, like many others, am often kept awake at night with this sole question:
You have access to headers. Headers are right next to the body of a request. Why can’t we get the body too?
Now, I am sure there are reasons as to why this isn’t a feature of the framework. First of all, rendering data due to a POST request is kind of weird. You certainly can do this, but when the user refreshes the page, that data will be gone. Still, there are some situations where you would want to render a page differently due to a form response.
Generally, the solution is to encode the form response in query parameters. However, the data might be too sensitive to put in query parameters, so then generally you reach for a JWT or some other nonsense to act as a key for the data that then has to be fetched every time the page is rendered. This is probably an industry best practice or something.
However, I don’t care about industry best practices. I’m unemployed. I am doing this for fun.
Please do not expect this feature from Next.js, and I strongly do not recommend making your own fork on Next.js that has this feature and using it in production.
If I can modify the source code to achieve this, I want to make a couple rules:
No placing the body somewhere in one part of the server and then reading it, unless it’s simply in memory. We can’t put it in a long running process or a file system.
Should work in prod mode, with default config
Let’s look at headers to see how it currently works.
next/src/server/request/headers.ts
This is pretty complicated. There is a lot of code that runs on the workUnitStore.
First of all, what workUnitStore are we using here? I can assume we are not using force static. With some debugging I found we are using request. This makes sense, since we are doing a dynamic live request of headers.
Looks like there is a branch here, at next/src/server/request/headers.ts#L146 where we check to see if in dev mode. If in dev, we have access to headers so we just pass them through.
In prod though, we have to check if InEarlyRenderStage? Apparently there are 7 stages of rendering within Next.js, see: staged-rendering.ts They both return headers though, although one is earlyHeaders. Well, let’s see how these headers are constructed.
For dev mode, it looks like the requestStore just has access to req.headers! next/src/server/async-storage/request-store.ts#L203 Can we also read req.body?
If we go to the lowest root of dev, we can find that Next.js is wrapping a Node HTTP server with requestListener next/src/server/lib/start-server.ts#L241
We can find that the NextRequest is actually constructed from IncomingMessage here: next/src/build/templates/app-page.ts#L711 . When the framework converts from a IncomingMessage to a NextRequest, it ignores body.
We know everything we need now in order to add our feature.
Since this is a vanilla Node Request, we actually have to write our own code to read body as a stream, like so:
It seems that all we have to do is add a field to reference body in the NextRequest, and then read that from workUnitStore.
This ends up needing to be done in a few places so it will compile with typescript, but only one place is interesting: next/src/export/routes/app-page.ts#L79
We also add a new getter to requestStore
Now we can read this body field from headers, and we should be able to get our data. WorkUnitStore is an instantiated requestStore.
I tried here to add a new function in headers.ts that let us read the body specifically, but for some reason, when Next.js compiled, it would not let me call the new function, so I’m going to just modify the framework headers code in place so that it now returns body.
In next/src/server/request/headers.ts
Voila, in dev mode, we can see our post body:
Now for prod mode.
If we look at the code, there are 2 branches:
When I run a normal prod server, workUnitStore.asyncApiPromises is undefined, so we can do the same modification we did for dev here to read the body.
And we see the same result! I don’t quite know how to deploy a Next.js server running with a custom version of Next.js, so I will assume that the local production build would be the same as on Vercel.
However, can we also get the workUnitStore.asyncApiPromises branch to execute as well?
I dug a bit, and I think it’s related to cacheComponents https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents. I turned on cacheComponents in my next.config and I modified the code so it would return body instead of headers, and the network code does actually show that it is returning the post body, but the page renders as a blank page.
I expected to see an error, or it working. Even though I made many modifications to Next.js, I think that they are somewhat unrelated. The code doesn’t really seem to care if it’s returning headers or body from the request.
I found that this is actually a bug - I made a reproduction where you have cacheComponents: true and you try to render a page from a POST request, then the rendering fails.
Of course I opened an issue for this, even though rendering due to POST isn’t officially supported by the framework, I think it may point to another issue related to the cacheComponents implementation.
Spoiler: this was 5x harder than the rest of this blog post. The part that makes this hard is the only thing that shows up is a blank page. There is no error, no obvious sign as to what is causing this. All we know is that cacheComponents, <Suspense>, and headers are involved.
If we look into how <Suspense> works we get this fairly scary warning right from the react docs:
Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented.
After reading the code in app-render.tsx a few times and leaving console.logs, I realized that the part that was having an issue was related to postponedState. Basically, the way that Next.js renders a page with <Suspense> is that it sends down the static HTML, with the fallback rendered. Then, as the Suspense chunks resolve, it sends them down with JS that replaces them in the HTML, which is postponedState.
For some reason, when we were rendering from POST we would only see the postponedState, not the entire page. If we change the method on the <form> to GET then we actually get an entire page. It’s quite unclear to me why this matters.
However, there’s a tiny fix where we ignore the postponedState path of rendering if the request method is POST. Now, I imagine that this causes errors elsewhere in the behavior of the framework, but for our entertainment purposes, this is ok.
The fix is, in the renderToStream function in app-render.tsx:
As a result of our new condition, Next.js does a regular dynamic render in this case, which is exactly what we want.
You can see all of the changes I made here: Git diff. If you want to run it locally you can do so by installing the dependencies to compile Next.js locally and then running pnpm build. Then:
I would think not, even though this hacked solution does not seem complicated, there seems to be a lot of potential issues with adding reading request body during render. For one, it completely circumvents the SPA nature of Next.js, because the browser just immediately renders the HTML it gets back from a form navigation, ignoring any previous state. Also, I imagine it introduces many opportunities for bugs, like what we have seen above.
Finally, it’s not how modern web development generally handles forms - we prefer to send a POST request using fetch and then navigate with the new data. There are situations where, for example, if you have an HTML form in an iframe where perhaps you simply can’t build it any other way, but that’s quite rare, and certainly not 99% of users need that.
I do wonder why Next.js route handlers can’t return react rendered JSX, but since they’re relatively new, maybe it’s something that will happen down the line.


