Compatibility
Published
Almost every network call in a modern application is configured the same way: a URL, then an object literal full of well-known key names. method, headers, body, credentials, signal. That object looks like your code and is not. It is a dictionary the platform reads by name, which puts it in the same category as the Intl options object, and it fails in the same quiet way -- except when it fails loudly with an error message that names a method you never wrote.
What was measured
The sample builds a Request the way an ordinary API client does: a POST to a JSON endpoint, with a Content-Type header, a custom X-Tenant header, a serialised body, an explicit redirect policy and keepalive set. It then builds a Response with an explicit status of 201 and a statusText of Created, and reads both back. Finally it constructs a Headers object from a plain literal and reads a header out of it by wire name.
None of that is a mock. Request, Response and Headers are real platform objects in current Node, backed by the same implementation that serves fetch, so the reader of the init dictionary genuinely lives outside the file being protected. That matters, because a sample that owns both halves of a contract will report that everything is fine no matter what renaming does to it.
Protected in five configurations -- the default target, the modern target, both gate profiles and the string-transform profile -- the output was identical every time. The method stayed POST, both headers came back with their values, redirect stayed error, keepalive stayed true, the response still reported 201 and Created, and the serialised body was unchanged on the wire.
That is the expected result and the reason for it is worth stating plainly. Header names, method names and URLs in this code are string literals. The string transforms change where a literal is stored and how it is encoded, not what it decodes to. There is no syntax here for a target-version setting to lower. What remains is member renaming, and that is where the whole of the interesting result lives.
A POST that the platform calls a GET
The sharpest arm is also the smallest. With a member pattern matching a single name -- method -- the protected file threw before it produced any output at all: TypeError: Request with GET/HEAD method cannot have body, raised inside the platform's Request constructor.
Read that error against the source and the problem is obvious in hindsight and baffling in the moment. The source says method: 'POST'. It says so on the line the stack trace points at. But the emitted file says {_0x1:'POST', ...}, and the platform, finding no property called method, applies the specified default, which is GET. It then finds a body, and a GET request with a body is not legal, so it complains -- accurately, about a request you never wrote.
This is a good failure in the sense that matters most: it happens at construction, immediately, and it stops. But the diagnostic points away from the cause. An engineer reading that message will go looking for the place their code sends a GET, and there is no such place. The value 'POST' is still sitting there in the emitted file, as a string, attached to a key nobody reads.
Widen the pattern and the failure moves. Matching method, headers, body, redirect and keepalive together threw a different error -- TypeError: Cannot read properties of undefined (reading 'get') -- because req.headers now resolves to a generated name that does not exist on a Request. Same cause, different symptom, and this one at least fails on the line that reads it.
The quiet arm: a response that silently becomes a 200
The Response side is where the silence comes back. A pattern matching status and statusText produced no exception at all. The measured output was status=undefined and statusText=undefined, and the third line, ok=true, was unchanged.
Two separate things went wrong there and they cancel each other out visually. The init dictionary lost its status key, so the platform built a response with the default status of 200 rather than the 201 that was asked for. And the read-back lost its status key too, so the code asking the response what its status was got undefined. The renaming was perfectly self-consistent across the file. The platform sat in the middle and did not participate.
That ok=true is the detail worth keeping. It stayed true because 200 is a successful status just as 201 is, so the one assertion a test is most likely to make about a response is exactly the one that keeps passing. Code that branches on res.status === 201 to distinguish a create from an update now takes the wrong branch, silently, in a build that throws nothing.
This is the same shape as the renamed cause on an error and the renamed Intl options key: consistent renaming, applied to a name that something outside the file reads in between the write and the read. The rule this series keeps returning to is that self-consistency is not sufficient. It only helps when nothing else is looking at the name.
The body goes out with generated key names
One arm of this measurement had to be fixed before it could be believed, and it is worth describing because the mistake is easy to make. The first version of the sample serialised a payload into the request body and never read it back. Renaming the payload's field names measured SAME -- truthfully, and uselessly, because nothing in the output depended on them.
With the wire string actually printed, the result is unambiguous. A pattern matching the payload fields sku and qty emitted a body of {"_0x1":"A1","_0x2":2}, while the in-file read of payload.sku still returned A1 correctly. The application is internally consistent and the server receives nonsense.
That failure belongs to serialisation rather than to fetch, and this site covers it in detail under JSON serialisation -- the point of repeating it here is that the request body is the most common place to meet it. An API client is exactly the kind of module where the object being renamed and the object being sent are the same object, and where nothing local ever notices.
The practical consequence is that a member pattern which is safe for your view layer may not be safe for your transport layer, because they are usually different files with the same option applied to both. If you rename members at all, the request and response models are the first place to check, and the check is to look at the bytes on the wire rather than at anything the client reports about itself.
What survived, and one reason it did
The control arm behaved as expected: a pattern matching only names the sample owns changed nothing observable. So did something less obvious. A pattern matching get -- the method used to read a header out of a Headers object -- measured identical on both targets.
That is not luck and it is not a general guarantee about platform methods. get is a contextual keyword, and this engine always writes an access to a keyword-named property in quoted bracket form, so h.get(...) is emitted as h["get"](...). A string is not a rename site, so the call survives.
The same mechanism is dangerous the moment the property is one you declare rather than one the platform provides, because then the declaration is renamed and the quoted access is not. A cache or router object with a get method of its own is the classic case. Treat this result as a description of why one specific call survived, not as a rule you can lean on.
The methods that are not keywords fail in the ordinary way. There is nothing subtle there: rename them and the first call is a TypeError. Loud, immediate, and much easier to diagnose than anything described in the two sections above.
How to check your own build
Three checks cover this surface and none of them need a live server. Construct one Request exactly as your client does and assert on req.method. Construct one Response with a non-default status and assert on the number. Serialise one request payload and compare the string, not the object.
That third one is the check teams skip, and it is the one that catches the quiet failure. Comparing objects will not do it, because both sides of an internally-consistent rename agree. Comparing the JSON text will, because the text is what the server sees.
If a request is misbehaving in a protected build and you want to know quickly whether this is the cause, log the init object with JSON.stringify at the call site rather than reading fields off it. Real key names mean the dictionary survived. Generated names such as _0x1 mean the dictionary was renamed, and the fix is to narrow the member pattern rather than to change the request.
The general mitigation is the one this site recommends everywhere: anchor member renaming to names your own application owns. An options dictionary handed to a platform constructor is not one of them, and neither is a payload that leaves the process.
Frequently asked questions
Does obfuscation break fetch requests?
Not on its own. A POST with two headers, a serialised body, an explicit redirect policy and keepalive, plus a Response with a custom status, came through five protection configurations byte-identical, including both target versions and the string-transform profile. Header names, methods and URLs are string literals, and the string transforms do not change what a literal decodes to.
Why does my protected build say a GET request cannot have a body?
Because member renaming reached the method key of your init object. The emitted call becomes new Request(url, {_0x1:'POST', body:...}), the platform finds no method property and applies its default of GET, and a GET with a body is not legal. The error names GET even though your source plainly says POST, because the value is still there attached to a key nothing reads.
Can member renaming change the status code my code sees?
Yes, and it does so without throwing. A pattern matching status and statusText produced status=undefined and statusText=undefined while ok stayed true, because the response was built with the default 200 rather than the requested 201 and the read-back asked for a renamed property. Code that branches on a specific status takes the wrong branch silently.
Are my request headers safe from renaming?
The header names themselves are, because they are string literals. The headers key of the init dictionary is not: rename it and the platform never sees your headers, and reading req.headers afterwards fails because the property does not exist under a generated name. Reading a header back with the get method happened to survive in this measurement, but only because get is a keyword and is written as a quoted bracket access.
Will renaming change what my API receives in the request body?
It will if the payload object's field names match your member pattern. Measured, a body serialised from an object whose fields were renamed went out as {"_0x1":"A1","_0x2":2} while every in-file read of that object still returned the right value. The application stays internally consistent and the server receives key names it does not recognise.
What is the cheapest way to be sure my HTTP layer is unchanged?
Assert on three things without a live server: the method of a constructed Request, the numeric status of a constructed Response, and the serialised body as a string rather than as an object. The string comparison is the one that matters, because an internally-consistent rename makes both sides of an object comparison agree while the bytes on the wire have changed.
Related reading