Skip to content

attach req as res.req #28673

Closed
Closed
@ianstormtaylor

Description

@ianstormtaylor

I'd like to propose attaching the IncomingMessage (req) object in an HTTP server to the ServerResponse (res) object:

res.req = req

Because it is often the case that when creating a response you need to access information related to the request itself.

(If you read on you'll notice that all of the popular frameworks do this under the covers, and that Node's internal logic also uses this connection, but that it's not available for smaller utilities in userland.)


The current state of things.

As things stand right now, for HTTP response-related logic you often end up needing to have access to the req object too to handle a variety of edge-case situations, which leads to either verbosity or inconsistency, and either way… confusion.

For example, imagine writing a sendJson helper:

function sendJson(res, status, json, options = {}) {
  const { spaces = 2, pretty = true } = options
  let string

  if (pretty) {
    string = JSON.stringify(json, value, spaces)
  } else {
    string = JSON.stringify(json)
  }

  res.statusCode = status
  res.setHeader('Content-Length', Buffer.byteLength(string))
  res.setHeader('Content-Type', 'application/json; charset=utf-8')
  res.end(string)
}

All seems fine. But this doesn't account for HEAD requests that should not include a body. To fix this you'd have to add (req, ...) to the signature, which is unintuitive. This is a frequent issue with HTTP-related utilities, and it part of the reason why you end up having to reach for a framework, instead of seeing a proliferation of small, useful HTTP functions.

All the major userland frameworks do this already.

This has already been established as a common practice in userland, because of how often you need to tweak a response based on the request.

Often times the req is needed only for HTTP-specific plumbing edge cases (like checking HEAD or OPTIONS requests) that are not immediately obvious to the user's mental model. Which is why it's helpful if HTTP logic can access it implicitly for the edge cases where it's required.

Most of these frameworks also do the reverse, attaching the response to the request for parallelism/consistency.

Technically, you can already do the reverse.

You can actually already do the reverse—accessing the res object from the req without a framework, by using the req.connection._httpMessage property. Now, this isn't exactly encouraged seeing as it's an undocumented property, but it shows that it's not out of the question to create these kinds of links between these two objects.

Why not always pass req into helpers?

A counter-argument would be to say that req should just be passed into any helper that needs to use properties from it. This sounds reasonable at first, but the issue with this approach is that it leads to mismatch with user expectations.

The solutions become inconsistent because access to the req isn't always required. For example, consider a few different helpers for setting the correct headers on a response:

  • A simple setHeader(res, name, value) helper doesn't need the req, because it just sets the value to whatever was passed in.
  • But a setCors(res, options) helper would actually need to be setCors(req, res, options) because you need to determine if the request used the OPTIONS method.
  • A setAuthenciate(res, scheme, options) helper also doesn't need the req object.
  • But a setRequestId(res, options) would need the req to default the ID in case it was already set.

The exact nature of the examples is irrelevant—they could be implemented in many different ways. But the point is that access to req is required in certain situations when building a response, but not in others. Which leads to APIs that are inconsistent in signature between (res, ...) and (req, res, ...), causing confusion.

And it's often required for low-level, edge-case HTTP behaviors that most users of these modules ideally don't need to think about, because they are HTTP plumbing.

You might argue that then all response-related helpers should include (req, res, ...) to keep it consistent. But if you consider things like sendJson(res, status, json, options), adding an extra (req, ...) to every arguments list becomes tedious. Especially so for helpers that don't use the req at all, and are just forced to add it for consistency for others. It feels pretty dumb to have to do getBasicAuth(req, res) just because the library needs consistency to handle ~10–20% cases.

This awkwardness is why the major HTTP frameworks for Node have all included a way to access the request from the response themselves. They know that for users it's much simpler to think about working on either the request or the response. But that edge cases require having access to both objects at once.

Node's core uses the "reference" itself internally.

When creating the res object in the first place, Node's core has access to req. And it actually does some of the same exact things that are being advocated for here, by calculating some internal logic for the response based on the request.

if (req.method === 'HEAD') this._hasBody = false;

node/lib/_http_server.js

Lines 155 to 156 in a013aa0

this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
this.shouldKeepAlive = false;

node/lib/_http_server.js

Lines 713 to 716 in a013aa0

if (req.headers.expect !== undefined &&
(req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
if (continueExpression.test(req.headers.expect)) {
res._expect_continue = true;

These are all internal situations where specific response-related logic is being calculated by the request metadata and attached to res for future use. It just happens that Node already has the req in scope when creating the res. But it would be nice for userland to be able to do the same sorts of things with the res only.

This would open up new userland abilities.

Right now HTTP-related pieces of userland are almost exclusively handled by large monolithic frameworks like Express or Hapi. This is partly because there doesn't exist a simpler, Lodash-like set of utilities for handling HTTP requests in a simple functional way. Being able to link req and res would make building an intuitive library like this much easier.

Not only does this allow for people to opt-out of using a framework. But, more importantly, it allows other utilities to be built in a framework-agnostic way.

You could imagine…

image

…a world where the frameworks themselves aren't necessary. This would be helpful Lambda-like situations, or for simply adding extra HTTP-aware logic to adjacent domains, or just for keeping things extremely simple while prototyping. A lot of different use cases would benefit.

A good example of this is Vercel's built-in HTTP modifications. These kinds of behaviors are happening all over the place in the FaaS world, and only further fragment Node's HTTP handling. It would be amazing if non-monolithic solutions to this problem could thrive.


In summary...

Attaching res.req is a simple addition to Node's core that would make writing HTTP servers without needing a framework easier, which is especially helpful for writing framework-agnostic utilities, or in Lambda-like environments where you need smaller dependencies, or in performance-critical situations where framework overhead is harmful. All major userland HTTP frameworks already all do this.

It would also make it possible for userland to step up and create a nice set of framework-agnostic helper modules (eg. setCors, or setRequestId), without forcing them to have more confusing APIs than their framework-coupled counterparts—since at the moment they can't benefit from the link.

I hope that all makes sense. I think this would a small change, but one that unlocks a lot of value in userland at many layers of the stack.

Thanks for reading!

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.httpIssues or PRs related to the http subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions