A Safer Exit From `request`: Incremental Migration for Legacy Node.js Services
Why changing an HTTP client is an application-compatibility project, and how to reduce the blast radius without overstating what a wrapper can fix.
Disclosure: I maintain request-to-axios, the compatibility wrapper discussed below. This article is a practical account of the migration problem and the tradeoffs in that wrapper, not an independent customer testimonial.
An old HTTP dependency is rarely isolated to one line of code. In a mature Node.js service, request may be wrapped by internal helpers, used as a duplex stream, configured through defaults(), paired with a cookie jar, and expected to return a particular callback body type. A direct switch to Axios can change all of those behaviors at once.
That makes the migration question more specific than “Which HTTP client is better?” It is: how do we remove a deprecated dependency while controlling behavioral change in a system whose integration surface is only partly documented?
Start With the Actual Dependency Graph
The upstream request project was deprecated in 2020. Its repository says no further changes are expected. npm still publishes request@2.88.2, but the package's age and deprecation are separate from a finding that a given application is vulnerable. Check the resolved graph and the code paths that use it.
I ran a fresh install of request@2.88.2 into an isolated directory and used npm's audit database on 2026-11-02. That resolved these notable versions:
| Package | Resolved version | Advisory and important condition |
|---|---|---|
request |
2.88.2 |
CVE-2023-28155: SSRF mitigation bypass via cross-protocol redirects. Relevant when a service depends on redirect restrictions to block requests to internal destinations. No fixed upstream request version is listed. |
form-data |
2.3.3 |
CVE-2025-7783: predictable multipart boundary generation using Math.random(). Exploitation requires an attacker to observe random outputs and influence multipart content. |
form-data |
2.3.3 |
CVE-2026-12143: CRLF injection when untrusted data is used as multipart field names or filenames. Fixed in maintained release lines, but request's old dependency range does not admit those patched versions. |
qs |
6.5.5 |
CVE-2025-15284: arrayLimit is not consistently enforced for bracket notation. The default parameterLimit caps practical input; the risk changes if an application raises that limit substantially. |
qs |
6.5.5 |
CVE-2026-82417: a denial of service in a parse then stringify flow with attacker-controlled constructor.isBuffer data when permissive parse options are used. This is not equivalent to every call to request's query serializer being exploitable. |
tough-cookie |
2.5.0 |
CVE-2023-26136: prototype pollution under a specific CookieJar configuration (rejectPublicSuffixes: false). |
uuid |
3.4.0 |
CVE-2026-41907: missing buffer bounds checks for UUID v3/v5/v6 APIs when a caller supplies a buffer and offset. A vulnerable package in a dependency graph does not prove that request itself calls the affected API with attacker-controlled values. |
npm reported five vulnerable package nodes in that audit; some nodes have more than one advisory. The important engineering distinction is between a vulnerable component in the tree and an exploitable path in the application. Review both. Also note that a clean audit is time-bound: new advisories may be published after the check.
The Hidden Cost of a Direct Rewrite
The public API may look small, but its contract is not. Common request patterns include:
- Callback form:
(error, response, body)rather than a promise returning one Axios response object. - Streams: piping a response to a file, or piping an upload into the request.
- Serialization:
qs,form,formData, and JSON-specific response parsing. - Defaults: shared headers, base URLs, timeouts, proxies, and agents.
- Session behavior: a cookie jar, redirects, and authentication.
A mechanical replacement can compile and still change a service's behavior. Status handling, redirect method changes, stream lifetimes, body encoding, and error shapes are exactly the sorts of details that integration tests should pin down.
A practical migration plan is incremental:
- Search for direct calls and local wrappers, including
pipe(),.form(),.auth(), anddefaults(). - Record request options by call site. Do not assume every caller uses the same subset.
- Add tests around the observable contract: status code, headers, body type, redirects, and upload/download behavior.
- Introduce the new transport behind the existing import where the compatibility surface fits.
- Migrate unsupported cases explicitly rather than silently accepting options that do nothing.
- Run
npm auditon the final lockfile and test against representative downstream services.
Using the Wrapper as a Transition Step
request-to-axios keeps a request-style callback and stream facade while using Axios for HTTP transport. To trial it without editing every require('request') statement first:
npm install request@npm:request-to-axios
Common support includes method aliases, callbacks, response streams, defaults(), uri/url/baseUrl, qs, JSON serialization and parsing, URL-encoded and multipart forms, immediate Basic/Bearer auth, redirect limits, TLS/agent options, and an in-memory cookie jar.
It is not a full clone. OAuth 1, Hawk, AWS signing, HTTP Signature, HAR 1.2, digest retry, multipart/related, custom redirect policy callbacks, and exact request proxy-tunnel behavior are not implemented. Redirect options such as followAllRedirects and followOriginalHttpMethod do not reproduce the original semantics. The built-in cookie jar is host-only and in-memory; it does not implement public-suffix behavior or custom stores. The compatibility reference lists the current boundary.
The point of a wrapper is not to claim that migration is finished. It is to preserve enough of the familiar interface to let a team migrate call sites in smaller, testable batches, while making the exceptions explicit. Some services may be better served by a direct Axios rewrite; others may benefit from an intermediate compatibility layer.
Security Is a Process, Not a Package Name
Replacing request does not automatically make an application secure. The caller still needs to constrain user-controlled destinations, inspect redirect behavior, protect credentials, keep TLS certificate verification enabled, and review the current dependency tree. The wrapper itself also has dependencies and should be audited on release and in consuming applications.
For SSRF-sensitive clients, a particularly important question is where the destination URL comes from and what a redirect can reach. A new HTTP library can fix a known implementation flaw, but application-level URL allowlists and redirect policies remain necessary defense in depth.
If this compatibility approach fits, review the package source, npm release, and known limitations. Test your own request patterns before switching production traffic. Feedback and reproducible gaps can be filed on the GitHub project.
Keywords: Node.js, JavaScript, Application Security, Technical Debt, Axios, Migration
About the author
- Prithwiraj Bose is a Technical Architect at TCS who specializes in enterprise application design, cloud computing, microservices modernization, and IoT solutions. Driven by a passion for continuous learning, he actively shares his technical expertise and creative pursuits through blogging and community engagement. Please visit his personal blog https://sribasu.com
Comments
Join the discussionNo comments yet. Be the first to comment.