-
Notifications
You must be signed in to change notification settings - Fork 15
v1.19.27 #907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
v1.19.27 #907
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /** | ||
| * Response.body ReadableStream Polyfill Tests | ||
| * | ||
| * React Native's fetch (whatwg-fetch) implements Body.text()/arrayBuffer()/json() | ||
| * but does NOT expose the streaming `Response.body` getter. Stellar SDK v16's | ||
| * feaxios bounded-fetch adapter (used by StellarToml.Resolver and | ||
| * Federation.Server whenever maxContentLength/maxRedirects are set) reads the | ||
| * response exclusively via `response.body.getReader()`. With `body` undefined the | ||
| * adapter fell back to an empty buffer, so every stellar.toml parsed to `{}` and | ||
| * federation lookups failed with "stellar.toml does not contain FEDERATION_SERVER | ||
| * field". | ||
| * | ||
| * Jest (V8/Node) provides a native streaming `Response.body`, so these tests run | ||
| * the polyfill against plain prototype objects lacking `body` to reproduce the | ||
| * React Native gap. | ||
| */ | ||
| type ArrayBufferBody = { arrayBuffer(): Promise<ArrayBuffer> }; | ||
|
|
||
| type ReadableLike = { | ||
| getReader(): { | ||
| read(): Promise<{ done: boolean; value?: Uint8Array }>; | ||
| }; | ||
| }; | ||
|
|
||
| let createResponseBodyStream: (response: ArrayBufferBody) => ReadableLike; | ||
| let installResponseBodyPolyfill: (target?: unknown) => void; | ||
|
|
||
| beforeAll(() => { | ||
| // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires | ||
| ({ | ||
| createResponseBodyStream, | ||
| installResponseBodyPolyfill, | ||
| } = require("../../src/polyfills/responseBody")); | ||
| }); | ||
|
|
||
| const encode = (text: string): ArrayBuffer => { | ||
| const { buffer, byteOffset, byteLength } = new TextEncoder().encode(text); | ||
| return buffer.slice(byteOffset, byteOffset + byteLength); | ||
| }; | ||
|
|
||
| const readStream = async (stream: ReadableLike): Promise<string> => { | ||
| const reader = stream.getReader(); | ||
| const chunks: Uint8Array[] = []; | ||
| let total = 0; | ||
|
|
||
| for (;;) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| if (value) { | ||
| chunks.push(value); | ||
| total += value.byteLength; | ||
| } | ||
| } | ||
|
|
||
| const merged = new Uint8Array(total); | ||
| let offset = 0; | ||
| chunks.forEach((chunk) => { | ||
| merged.set(chunk, offset); | ||
| offset += chunk.byteLength; | ||
| }); | ||
|
|
||
| return new TextDecoder().decode(merged); | ||
| }; | ||
|
|
||
| describe("Response.body polyfill", () => { | ||
| it("createResponseBodyStream streams the body that arrayBuffer() returns", async () => { | ||
| // A real stellar.toml snippet: the streamed bytes must round-trip intact so | ||
| // the SDK's smol-toml parse can recover FEDERATION_SERVER, the field whose | ||
| // absence broke federation resolution. | ||
| const toml = 'FEDERATION_SERVER = "https://stellar.org/federation"\n'; | ||
| const response = { arrayBuffer: () => Promise.resolve(encode(toml)) }; | ||
|
|
||
| const text = await readStream(createResponseBodyStream(response)); | ||
|
|
||
| expect(text).toBe(toml); | ||
| }); | ||
|
|
||
| it("createResponseBodyStream surfaces arrayBuffer() rejection as a stream error", async () => { | ||
| const failure = new Error("network down"); | ||
| const response = { arrayBuffer: () => Promise.reject(failure) }; | ||
|
|
||
| await expect( | ||
| readStream(createResponseBodyStream(response)), | ||
| ).rejects.toThrow("network down"); | ||
| }); | ||
|
|
||
| it("installs a working Response.body getter when one is missing", async () => { | ||
| const toml = 'FEDERATION_SERVER = "https://example.com/federation"\n'; | ||
| const prototype: ArrayBufferBody = { | ||
| arrayBuffer: () => Promise.resolve(encode(toml)), | ||
| }; | ||
| const target = { prototype } as unknown as typeof Response; | ||
|
|
||
| expect(Object.getOwnPropertyDescriptor(prototype, "body")).toBeUndefined(); | ||
|
|
||
| installResponseBodyPolyfill(target); | ||
|
|
||
| const response = Object.create(prototype) as { body: ReadableLike | null }; | ||
| expect(response.body).not.toBeNull(); | ||
| expect(await readStream(response.body as ReadableLike)).toBe(toml); | ||
| }); | ||
|
|
||
| it("memoizes the stream so the single-use body is not consumed twice", () => { | ||
| const prototype: ArrayBufferBody = { | ||
| arrayBuffer: () => Promise.resolve(encode('FEDERATION_SERVER = "x"')), | ||
| }; | ||
| const target = { prototype } as unknown as typeof Response; | ||
|
|
||
| installResponseBodyPolyfill(target); | ||
|
|
||
| const response = Object.create(prototype) as { body: ReadableLike }; | ||
| expect(response.body).toBe(response.body); | ||
| }); | ||
|
|
||
| it("does not override a runtime that already provides Response.body", () => { | ||
| const nativeStream = Symbol("native-body"); | ||
| const prototype: ArrayBufferBody = { | ||
| arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), | ||
| }; | ||
| Object.defineProperty(prototype, "body", { | ||
| configurable: true, | ||
| get: () => nativeStream, | ||
| }); | ||
| const target = { prototype } as unknown as typeof Response; | ||
|
|
||
| installResponseBodyPolyfill(target); | ||
|
|
||
| const response = Object.create(prototype) as { body: unknown }; | ||
| expect(response.body).toBe(nativeStream); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /** | ||
| * Response.body ReadableStream Polyfill for React Native | ||
| * | ||
| * React Native's fetch (whatwg-fetch) implements the Body consumers | ||
| * `text()`/`arrayBuffer()`/`json()` but never exposes the streaming | ||
| * `Response.body` getter, so `response.body` is `undefined`. | ||
| * | ||
| * The Stellar SDK (v16+) routes HTTP through `feaxios`. Whenever a request sets | ||
| * `maxContentLength` or `maxRedirects` it takes the SDK's bounded-fetch adapter, | ||
| * which reads the response body *exclusively* via `response.body.getReader()`. | ||
| * `StellarToml.Resolver.resolve()` and `Federation.Server` both set those | ||
| * options, so on React Native the adapter saw `body === undefined`, fell back to | ||
| * an empty buffer, and every stellar.toml parsed to `{}`. Federation lookups then | ||
| * failed with "stellar.toml does not contain FEDERATION_SERVER field". | ||
| * | ||
| * This polyfill backs the missing getter with the `arrayBuffer()` that React | ||
| * Native does support, exposing a single-chunk `ReadableStream`. It is guarded so | ||
| * it no-ops on runtimes (Node/V8 in Jest, future RN) that already provide | ||
| * `Response.body`. | ||
| * | ||
| * `ReadableStream` is imported from `web-streams-polyfill` because Hermes does not | ||
| * provide a global one. | ||
| */ | ||
| import { ReadableStream } from "web-streams-polyfill"; | ||
|
|
||
| /** The subset of `Response` this polyfill relies on. */ | ||
| interface ArrayBufferBody { | ||
| arrayBuffer(): Promise<ArrayBuffer>; | ||
| } | ||
|
|
||
| /** | ||
| * Builds a one-shot `ReadableStream` that emits the response body as a single | ||
| * `Uint8Array` chunk, sourced from `arrayBuffer()`. A rejected `arrayBuffer()` | ||
| * surfaces as a stream error so callers see the original failure. | ||
| */ | ||
| export const createResponseBodyStream = ( | ||
| response: ArrayBufferBody, | ||
| ): ReadableStream<Uint8Array> => { | ||
| const bodyPromise = response.arrayBuffer(); | ||
|
|
||
| return new ReadableStream<Uint8Array>({ | ||
| start(controller) { | ||
| bodyPromise | ||
| .then((buffer) => { | ||
| controller.enqueue(new Uint8Array(buffer)); | ||
| controller.close(); | ||
| }) | ||
| .catch((error: unknown) => controller.error(error)); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Defines a lazy `body` getter on the target's prototype when it is missing. The | ||
| * stream is memoized per response instance so repeated access returns the same | ||
| * stream and the single-use body is not consumed twice. | ||
| */ | ||
| export const installResponseBodyPolyfill = ( | ||
| ResponseCtor: typeof Response | undefined = typeof Response === "undefined" | ||
| ? undefined | ||
| : Response, | ||
| ): void => { | ||
| if (!ResponseCtor) { | ||
| return; | ||
| } | ||
|
|
||
| if (Object.getOwnPropertyDescriptor(ResponseCtor.prototype, "body")) { | ||
| return; | ||
| } | ||
|
|
||
| Object.defineProperty(ResponseCtor.prototype, "body", { | ||
| configurable: true, | ||
| get(this: ArrayBufferBody): ReadableStream<Uint8Array> { | ||
| const stream = createResponseBodyStream(this); | ||
| Object.defineProperty(this, "body", { | ||
| configurable: true, | ||
| value: stream, | ||
| }); | ||
| return stream; | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| installResponseBodyPolyfill(); | ||
|
|
||
| export {}; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When callers only probe or cancel
response.body, this startsarrayBuffer()immediately, before any reader actually reads from the stream. That affects existing early-error paths such asfetchMetadataJson's redirect/non-OK/oversized-Content-Length branches, which callresponse.body?.cancel?.()to discard the response; with this getter installed, those paths now begin buffering/converting the body they are trying to reject, so a large error/redirect response can allocate the full body before throwing. DeferringarrayBuffer()until the first pull/read, or making cancellation before the first read avoid starting it, would preserve the fail-fast behavior.Useful? React with 👍 / 👎.