diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 4f6a7d92..4f911e7b 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -26,12 +26,16 @@ function requestTimeout(timeoutInMs: number = 0): Promise { } function normaliseRequestBody(body: unknown): string | ArrayBuffer | undefined { + if (body === undefined) return undefined; if (typeof body === "string" || body instanceof ArrayBuffer) return body; if (ArrayBuffer.isView(body)) { - if (body.buffer instanceof ArrayBuffer) return body.buffer; - return new Uint8Array(body.buffer).slice().buffer; + if (body.buffer instanceof ArrayBuffer && body.byteOffset === 0 && body.byteLength === body.buffer.byteLength) { + return body.buffer; + } + return new Uint8Array(body.buffer, body.byteOffset, body.byteLength).slice().buffer; } - return undefined; + const bodyType = Object.prototype.toString.call(body).slice(8, -1); + throw new TypeError(`Obsidian requestUrl does not support the request body type ${bodyType}`); } /** diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.unit.spec.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.unit.spec.ts new file mode 100644 index 00000000..110f78db --- /dev/null +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.unit.spec.ts @@ -0,0 +1,60 @@ +import { HttpRequest } from "@smithy/protocol-http"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const requestUrlMock = vi.hoisted(() => + vi.fn< + (param: { body?: string | ArrayBuffer }) => Promise<{ + headers: Record; + status: number; + arrayBuffer: ArrayBuffer; + }> + >() +); + +vi.mock("@/deps.ts", () => ({ + requestUrl: requestUrlMock, +})); + +import { ObsHttpHandler } from "./ObsHttpHandler.ts"; + +function requestWithBody(body: unknown) { + return new HttpRequest({ + protocol: "https:", + hostname: "objects.example.com", + method: "PUT", + path: "/bucket/object", + headers: {}, + body, + }); +} + +describe("ObsHttpHandler request bodies", () => { + beforeEach(() => { + requestUrlMock.mockReset(); + requestUrlMock.mockResolvedValue({ + headers: {}, + status: 200, + arrayBuffer: new ArrayBuffer(0), + }); + }); + + it("sends only the bytes addressed by an ArrayBuffer view", async () => { + const body = new Uint8Array([0, 1, 2, 3]).subarray(1, 3); + + await new ObsHttpHandler().handle(requestWithBody(body)); + + expect(requestUrlMock).toHaveBeenCalledOnce(); + const transmittedBody = requestUrlMock.mock.calls[0][0].body; + expect(transmittedBody).toBeInstanceOf(ArrayBuffer); + expect([...new Uint8Array(transmittedBody as ArrayBuffer)]).toEqual([1, 2]); + }); + + it("rejects an unsupported body instead of dispatching an empty request", async () => { + const body = new ReadableStream(); + + await expect(new ObsHttpHandler().handle(requestWithBody(body))).rejects.toThrow( + "Obsidian requestUrl does not support the request body type ReadableStream" + ); + expect(requestUrlMock).not.toHaveBeenCalled(); + }); +});