Skip to content

Commit 5e0beb9

Browse files
committed
Merge remote-tracking branch 'origin/main' into armorer/fs-http-timeout-surface
2 parents cb48503 + eeb39e2 commit 5e0beb9

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

packages/http/src/http.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const HEADERS_TO_TYPE: Record<string, string> = {
2727
*/
2828
export const DEFAULT_TIMEOUT_MS = 30_000;
2929

30+
// axios 1.15+ types `response.headers[key]` as `AxiosHeaderValue | undefined`
31+
// (string | string[] | number | boolean | null | AxiosHeaders | undefined). For
32+
// content-type handling we only honor a real string; any other shape falls back
33+
// to undefined so the consumer's default applies.
34+
const asString = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);
35+
3036
const unregister = <T>(array: T[], item: T): UnregisterMiddleware => {
3137
return () => {
3238
const index = array.indexOf(item);
@@ -109,7 +115,7 @@ export const createHttpService = (baseURL: string, options?: HttpServiceOptions)
109115
const response = await http.get(endpoint, {responseType: 'blob'});
110116
const {data, headers} = response;
111117

112-
const actualType = getContentType(headers['content-type'], type);
118+
const actualType = getContentType(asString(headers['content-type']), type);
113119

114120
const blob = new Blob([data], {type: actualType});
115121
const link = document.createElement('a');
@@ -126,7 +132,7 @@ export const createHttpService = (baseURL: string, options?: HttpServiceOptions)
126132

127133
const previewRequest = async (endpoint: string): Promise<string> => {
128134
const response = await http.get(endpoint, {responseType: 'blob'});
129-
const contentType: string = response.headers['content-type'] ?? 'application/octet-stream';
135+
const contentType = asString(response.headers['content-type']) ?? 'application/octet-stream';
130136
const blob = new Blob([response.data], {type: contentType});
131137

132138
return window.URL.createObjectURL(blob);

packages/http/tests/http.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,17 @@ describe('createHttpService', () => {
532532
);
533533
});
534534

535+
it('throws when content-type header is non-string (axios 1.15+ AxiosHeaderValue)', async () => {
536+
// Arrange — axios 1.15+ types header values as
537+
// `AxiosHeaders | string | string[] | number | boolean | null`. Non-string values
538+
// fall through `asString` to undefined, hitting the same path as a missing header.
539+
mock.onGet(`${BASE_URL}/download/odd`).reply(200, 'data', {'content-type': null as unknown as string});
540+
const service = createHttpService(BASE_URL);
541+
542+
// Act & Assert
543+
await expect(service.downloadRequest('/download/odd', 'file.bin')).rejects.toThrow('No content type found');
544+
});
545+
535546
it('revokes the object URL after triggering the download', async () => {
536547
// Arrange
537548
mock.onGet(`${BASE_URL}/download/file.pdf`).reply(200, 'file-content', {'content-type': 'application/pdf'});
@@ -577,6 +588,22 @@ describe('createHttpService', () => {
577588
const BlobMock = globalThis.Blob as unknown as ReturnType<typeof vi.fn>;
578589
expect(BlobMock).toHaveBeenCalledWith(['blob-data'], {type: 'application/octet-stream'});
579590
});
591+
592+
it('uses fallback content type when header is non-string (axios 1.15+ AxiosHeaderValue)', async () => {
593+
// Arrange — axios 1.15+ types header values as a union including arrays, numbers,
594+
// booleans, and null. Any non-string value falls through `asString` and hits the
595+
// same fallback path as a missing header.
596+
mock.onGet(`${BASE_URL}/preview/doc`).reply(200, 'blob-data', {'content-type': null as unknown as string});
597+
const service = createHttpService(BASE_URL);
598+
599+
// Act
600+
const url = await service.previewRequest('/preview/doc');
601+
602+
// Assert
603+
expect(url).toBe('blob:http://localhost/fake-object-url');
604+
const BlobMock = globalThis.Blob as unknown as ReturnType<typeof vi.fn>;
605+
expect(BlobMock).toHaveBeenCalledWith(['blob-data'], {type: 'application/octet-stream'});
606+
});
580607
});
581608

582609
describe('streamRequest', () => {

0 commit comments

Comments
 (0)