Skip to content

Migrate networking library from postman-request to needle#282

Open
TwitchBronBron wants to merge 15 commits into
masterfrom
migrate-postman-request-to-needle
Open

Migrate networking library from postman-request to needle#282
TwitchBronBron wants to merge 15 commits into
masterfrom
migrate-postman-request-to-needle

Conversation

@TwitchBronBron

@TwitchBronBron TwitchBronBron commented Jun 3, 2026

Copy link
Copy Markdown
Member

What

Replaces the unmaintained postman-request with needle.

Non-breaking. roku-deploy attaches a request-shaped results/response object to thrown errors and return values, so a thin shim (src/request.ts) reconstructs that exact shape on top of needle — response.statusCode, response.headers, response.request.host, and a string body.

Also drops ~860 lines from the lockfile.

How

The shim exposes the slice of the request API roku-deploy uses — post(opts, cb), get(opts, cb), and the callback-less streaming get(opts).on(...).pipe(...) — and translates it to needle. Digest auth is now native to needle (auth: 'digest'), matching postman-request's challenge/response behavior.

Four quirks surfaced while validating against a real Roku, each handled in the shim:

  • Empty multipart valuesrequest silently dropped empty fields; needle throws "value missing for multipart!". The shim drops them.
  • Bodyless POSTs — needle throws "Empty multipart body" on an empty body (e.g. ECP keypress in pressHomeButton), so multipart is only enabled when form data is actually present.
  • Buffer body — under parse_response: false needle returns a Buffer; request returned a string and checkRequest guards on typeof body === 'string'. The shim coerces to a string.
  • Streaming digest dance — needle emits an intermediate 401 response before retrying with Authorization; getToFile treats any non-200 as failure. The shim swallows that one intermediate 401.

Testing

  • Unit suite: all passing; spec stubs target needle directly.
  • Real device: src/device.spec.ts exercises every networking-enabled RokuDeploy method against a real Roku, with type/shape-tolerant assertions so it won't break as device data changes. Signing and disruptive (reboot / update-check) tests are gated behind ROKU_SIGNING_PASSWORD / ROKU_RUN_DISRUPTIVE=1.

Validated end-to-end against a device: getDeviceInfo, getDevId, getEcpNetworkAccessMode, validateDeveloperPassword, pressHomeButton, deploy/publish (digest + multipart zip upload), takeScreenshot (POST + stream-to-file), convertToSquashfs, deleteInstalledChannel, deleteComponentLibrary/deleteAllComponentLibraries, and the firmware-gated rebootDevice/checkForUpdate.

🤖 Generated with Claude Code

TwitchBronBron and others added 6 commits June 25, 2026 03:43
Replace the unmaintained `postman-request` dependency with `needle`. To keep
this a non-breaking change, a thin compatibility shim (`src/request.ts`)
reconstructs the `request`/`postman-request`-style response shape that
roku-deploy exposes on results and thrown errors (`results.response.statusCode`,
`results.response.headers`, `results.response.request.host`, string `results.body`).

The shim handles three quirks discovered while validating against a real Roku
device: needle rejects empty multipart values (request dropped them), needle
returns a Buffer body under `parse_response: false` (request returned a string),
and needle emits an intermediate 401 `response` event during the streaming
digest dance (request only surfaced the final response). It also only enables
multipart when form data is actually present, so bodyless POSTs (e.g. ECP
keypress) keep working.

Adds full real-device test coverage over every networking-enabled RokuDeploy
method in `device.spec.ts`, with structure/type-tolerant assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e shim

The `results`/`response` object that roku-deploy attaches to thrown errors is
part of its public API; consumers read specific paths off it
(`results.response.statusCode`, `.headers.server`, `.request.host`, string
`results.body`). Getting any of these wrong during the needle migration would be
a silent breaking change.

- Add a `needleClient` seam to request.ts so the shim's HTTP calls are stubbable
  (mirrors the `httpClient` seam in fetch.ts).
- Add request.spec.ts: unit tests for option translation (qs, digest auth,
  timeouts, headers), formData/multipart handling (drop empty fields, stream ->
  file form, no multipart for bodyless POSTs), body coercion (Buffer -> string),
  response reshape (statusCode/headers/request.host/href), error passthrough, and
  the streaming get path (err->error bridge, intermediate-401 suppression).
- Add an "error results structure" block to RokuDeploy.spec.ts that drives
  RokuDeploy through the REAL shim and pins the results shape for each error type:
  UnauthorizedDeviceResponseError, InvalidDeviceResponseCodeError,
  UnparsableDeviceResponseError, FailedDeviceResponseError,
  EcpNetworkAccessModeDisabledError, UpdateCheckRequiredError, ConnectionResetError.

Also hardens buildResponse to pass through a missing response object instead of
crashing, preserving checkRequest's `!results.response` guard behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI enforces 100% coverage; the new shim had a few uncovered branches. Add
targeted tests for: the username/password auth alias, null/undefined qs values
(and an all-null qs leaving the url untouched), null and non-string/non-buffer
body coercion, an unparseable url leaving request.host undefined, and a
digest-auth 'response' event carrying no response object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…testing

Ran an empirical parity check: installed the last postman-request release (3.17.6)
alongside the new needle build and triggered every device-reproducible failure
mode against a real Roku, diffing the thrown error structures.

7/8 were already byte-identical. The one real gap: postman-request attached the
(string) body to `response.body` in addition to returning it as the callback's
3rd arg, but the shim left `response.body` undefined. A consumer reading
`error.results.response.body` would have broken. Fixed buildResponse to set it.

(The 8th difference is the raw network error on a dead host -- ETIMEDOUT vs
ECONNRESET -- which is the underlying HTTP library's own error passed through
untouched; the contract, a bare Error with no results, is identical.)

Pin the findings with permanent, device-free unit tests that deep-equal the FULL
response/results structure the parity run confirmed (request.spec.ts and the
RokuDeploy.spec.ts error-results block).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The widened buildResponse (full IncomingMessage + maximized request object) had
two coverage-blocking spots that were over-engineering, not real safeguards:
 - a try/catch around `url.parse()`, whose catch was unreachable (`url.parse` is
   total for string input — a bare token yields null host/hostname, never throws),
 - an acronym special-case in the header title-caser (WWW/TE/DNT/ETag), which only
   applies to response headers, not the outgoing-request headers this touches.

Removed both, and updated the request.spec assertions to match the parity-faithful
shape that url.parse produces: response.request.host is the hostname (no port),
the port lives on response.request.uri.host/uri.port, unparseable urls yield null
(matching postman-request, which also used url.parse), and outgoing headers are
title-cased.

preversion is green: build + lint + 421 tests passing + 100% coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TwitchBronBron TwitchBronBron force-pushed the migrate-postman-request-to-needle branch from 637dfda to 7ae80ae Compare June 25, 2026 07:47
Comment thread src/device.spec.ts
@TwitchBronBron TwitchBronBron changed the title Migrate from postman-request to needle Migrate networking library from postman-request to needle Jul 1, 2026
@TwitchBronBron TwitchBronBron changed the title Migrate networking library from postman-request to needle Migrate networking library from postman-request to needle Jul 1, 2026
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drop postman-request in favor of native fetch

2 participants