Compatibility

Does Obfuscation Break HTTP Request Handlers?

Server-side JavaScript gets protected too: desktop applications embed it, edge functions run it, and licensed on-premise products ship it. A request handler is an unusually pure example of the contract problem, because almost nothing it reads was written by the program reading it. The headers came from the runtime, the query came from a URL, and the body came from whoever called you.

What was measured

One sample file that starts a real server on a loopback port, sends itself a real request, and prints both sides. It reads two headers the runtime built from the wire, turns the query string into an object, dispatches through a route table keyed by method and path, parses a JSON body that arrives as text, runs a required-field check, writes a response header dictionary, and finally prints what the client actually received.

The request body and the request path are pasted in as text, exactly as they would arrive from a caller. This matters more than it sounds: a sample that builds a body from an object literal and then reads it back renames both halves together and reports that everything is fine, truthfully and uselessly. Here the party that writes each key is either the runtime or the caller, and never the file under test.

The base column is clean. All five protection profiles reproduced the unprotected output exactly, including the status code, the response headers the client saw and the reply body. Protection alone does not touch request handling.

Query values and body fields read undefined, and the reply loses them

With a pattern matching two query parameter names, both reads returned undefined. The query object was built by iterating the parsed URL, so its keys came from the text of the request; the reads were rewritten to generated names. The key list printed before those reads was unchanged, which is the same split seen everywhere in this series: the data is right there and the code is asking for something else.

The body arm carries the consequence outward. With a pattern matching two fields of the caller's JSON document, both reads came back undefined and the reply the client received went from three fields to one. The two renamed fields were not sent under generated names -- they were absent, because a property whose value is undefined is omitted during serialisation.

The required-field check on that same body never moved. It reported nothing missing in every arm, because it tests a list of strings against keys the caller wrote, and neither of those is a rename site. The validator that exists to catch a malformed request is structurally unable to see this one.

Renaming the response status property gives the mirror image on the client side: the reply was correct and complete, and the code that read its status printed undefined. Every test of the form status equals 201 then returns false, so a successful request is reported as a failure by the only line that inspects it.

A container throws at the first access, which is the good outcome

One arm fails immediately. Renaming the headers container produced a type error on the first header read, before anything else in the handler ran. Nothing was logged, nothing was served, and the process reported a name it could not read.

That is the loud half of the container-versus-leaf split. Rename a container that things are read out of and the failure arrives at the first access, in the stack trace, on the line that did it. Rename a leaf and the program continues with the wrong value. Readers usually assume the opposite: that a small change fails small. It is the other way round, and the small change is the one that ships.

The sharpest arm: a request that goes to a different server

The client options dictionary is the arm worth the whole page. With a pattern matching the host, port, path and method keys of the options object handed to the request call, the request still went out -- to somewhere else entirely.

The mechanism is that none of those keys are required. A request options object with no recognised host, port or path falls back to documented defaults: the local host, port 80, the root path and a GET. The renamed keys are still in the emitted file, holding the correct values, attached to names nothing reads. So the runtime built a perfectly valid request using its defaults and sent it to a machine and a path the source never mentions.

In the measured run the answer came back from a completely different program that happened to be listening on port 80 of the same machine, and the client printed a 500 status with an HTML error page from that other server. The value of the arm is not the specific page: it is that the client received a real, well-formed HTTP response from a server the code never intended to contact, and that a status-code check would have reported an ordinary upstream failure.

The same run also flipped a read of the listening port from a number to undefined, which is the kind of detail that turns a log line into a mystery. If a build starts talking to localhost when the configuration clearly names a remote endpoint, look at what your member pattern matches before you look at DNS. The client side of this contract is covered in more depth in the article on fetch and request options; the mechanism there is the same and the failure that arm produced was a POST the platform treated as a GET.

Route tables, response headers and the parts that survive

Some of the handler is immune, for reasons worth knowing. The route table is an object keyed by strings and looked up with a string built from the request, so no renaming applies and dispatch was correct in every arm. Header names in a response dictionary are strings for the same reason, and the client saw them unchanged.

The method and the path read off the incoming request are properties the runtime writes, so a pattern matching them breaks the reads and the dispatch that depends on them. That combination -- string keys safe, property reads not -- means half of a router keeps working while the other half stops, which is why the symptom is usually a route that falls through to a not-found branch rather than a server that fails to start.

The reply object your handler builds is entirely yours, so renaming it is internally consistent and only the wire format moves. The measured arm produced a reply carrying a generated key in place of the identifier the caller expects. Nothing in the process notices, because the process is not the thing that reads it.

Individual header reads are the clearest example of the string rule holding under pressure. Both header values in the sample are read by string subscript, and both stayed correct in every arm where the process survived, including arms that broke the body and the query around them. The header name is text; the container holding the headers is a property. Rename the text and nothing happens, because there is no text to rename; rename the container and the handler stops at its first line.

How to check a handler in five minutes

Start the protected build, send it one request with a fixed body, and print three things: the value of one field read off the parsed body, the serialised reply, and the status the client saw. Compare each against a literal you typed rather than against another read from the same object, because two renamed reads compare equal to each other while both are wrong.

Then check where the request went. Print the resolved host and port your client used, not the configuration you passed in. That single line separates a routing problem from a renaming problem, and it is the one measurement this page would not have produced without a real server on the other end.

If you need renaming on a service, anchor the pattern to your own internal field names and keep it away from anything named by the protocol: header names, status fields, method and path keys, and the field names in any document a caller sends you. Those are contracts with parties that will never see your build.

Frequently asked questions

Does obfuscation break a Node HTTP server?

Not in the default configuration. A sample that starts a real server, sends itself a real request, reads headers and a query string, dispatches through a route table, parses a JSON body and writes a response produced identical output on all five protection profiles measured. Handlers only become a surface when member renaming matches names written by the caller or the runtime.

Why do my query parameters read as undefined after obfuscation?

Because the query object is built from the text of the request URL at run time, which is not a rename site, while the code reading it is. The emitted file asks for a generated name that the parsed query does not carry. In the measured run the values came back undefined and the printed key list stayed correct, which is the quickest way to see the mismatch.

Why is my API response missing fields after protection?

Because a renamed read produces undefined and undefined-valued properties are omitted during serialisation. The measured reply went from three fields to one. The fields are not sent under generated names, they are simply absent, so the caller reports a required property missing on something your source clearly sets.

Can member renaming send my request to the wrong server?

Yes, and this was the sharpest arm measured. Renaming the host, port, path and method keys of a request options object leaves the runtime with no recognised values, so it falls back to its documented defaults: the local host, port 80 and the root path. The measured client received a real response from an unrelated program listening on that port.

Why does my status code read as undefined while the request succeeds?

Because the response is correct and only the property read was renamed. The server sent the right status, the client received it, and the line inspecting it asked for a name the response object does not have. Any test comparing the status against a number then fails on a request that actually worked.

Which parts of a request handler survive member renaming?

Anything reached by a string. A route table keyed by strings, header names in a response dictionary, and required-field lists all survived every arm, because a string literal is not an identifier. Property reads off the request, the response and any parsed document do not survive, which is why half a router can keep working while the other half stops.

How do I verify a protected service quickly?

Send one request with a fixed body and print three things: one field read off the parsed body, the serialised reply, and the status the client saw. Compare each against a literal you typed rather than against another read of the same object, because two renamed reads agree with each other while both are wrong. Then print the host and port your client actually resolved.

Related reading