Skip to content

Commit 3cfb911

Browse files
committed
byteLengthFromUrl should take an options object
1 parent ea6755e commit 3cfb911

3 files changed

Lines changed: 29 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Previous `parquetMetadata` is now `import { parquetMetadataSync } from 'hyparquet/src/metadata.js'`
66
- Rename `onPage.columnName: string` to `onPage.pathInSchema: string[]`
77
- Remove `rowFormat` and always return rows as objects
8+
- Change `byteLengthFromUrl` to accept options object
89

910
## [1.23.0]
1011
- Replace `columnName: string` with `pathInSchema: string[]` in `onPage` callback (#144)

src/utils.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,18 @@ export function equals(a, b) {
5757
* Get the byte length using fetch with a ranged GET request.
5858
* Aborts the request if server returns 200 instead of 206.
5959
*
60-
* @param {string} url
61-
* @param {RequestInit} [requestInit] fetch options
62-
* @param {typeof globalThis.fetch} [fetchFn] fetch function to use
60+
* @param {object} options
61+
* @param {string} options.url
62+
* @param {RequestInit} [options.requestInit] fetch options
63+
* @param {typeof globalThis.fetch} [options.fetch] fetch function to use
6364
* @returns {Promise<number>}
6465
*/
65-
async function byteLengthFromUrlUsingFetch(url, requestInit = {}, fetchFn = globalThis.fetch) {
66+
async function byteLengthFromUrlUsingFetch({ url, requestInit = {}, fetch = globalThis.fetch }) {
6667
const controller = new AbortController()
6768
const headers = new Headers(requestInit.headers)
6869
headers.set('Range', 'bytes=0-0')
6970

70-
const res = await fetchFn(url, {
71+
const res = await fetch(url, {
7172
...requestInit,
7273
headers,
7374
signal: controller.signal,
@@ -106,25 +107,26 @@ async function byteLengthFromUrlUsingFetch(url, requestInit = {}, fetchFn = glob
106107
* If HEAD succeeds but Content-Length is missing, falls back to GET with range.
107108
* If requestInit is provided, it will be passed to fetch.
108109
*
109-
* @param {string} url
110-
* @param {RequestInit} [requestInit] fetch options
111-
* @param {typeof globalThis.fetch} [customFetch] fetch function to use
110+
* @param {object} options
111+
* @param {string} options.url
112+
* @param {RequestInit} [options.requestInit] fetch options
113+
* @param {typeof globalThis.fetch} [options.fetch] fetch function to use
112114
* @returns {Promise<number>}
113115
*/
114-
export async function byteLengthFromUrl(url, requestInit, customFetch) {
116+
export async function byteLengthFromUrl({ url, requestInit, fetch: customFetch }) {
115117
const fetch = customFetch ?? globalThis.fetch
116118
const res = await fetch(url, { ...requestInit, method: 'HEAD' })
117119

118120
// If HEAD request is forbidden (common with signed S3 URLs), try GET with range
119121
if (res.status === 403) {
120-
return byteLengthFromUrlUsingFetch(url, requestInit, fetch)
122+
return byteLengthFromUrlUsingFetch({ url, requestInit, fetch })
121123
}
122124

123125
if (!res.ok) throw new Error(`fetch head failed ${res.status}`)
124126
const length = res.headers.get('Content-Length')
125127
// If Content-Length is missing from HEAD, fallback to GET with range
126128
if (!length) {
127-
return byteLengthFromUrlUsingFetch(url, requestInit, fetch)
129+
return byteLengthFromUrlUsingFetch({ url, requestInit, fetch })
128130
}
129131
return parseInt(length)
130132
}
@@ -146,7 +148,7 @@ export async function asyncBufferFromUrl({ url, byteLength, requestInit, fetch:
146148
if (!url) throw new Error('missing url')
147149
const fetch = customFetch ?? globalThis.fetch
148150
// byte length from HEAD request
149-
byteLength ??= await byteLengthFromUrl(url, requestInit, fetch)
151+
byteLength ??= await byteLengthFromUrl({ url, requestInit, fetch })
150152

151153
/**
152154
* A promise for the whole buffer, if range requests are not supported.

test/utils.test.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ describe('byteLengthFromUrl', () => {
4545
headers: new Map([['Content-Length', '1024']]),
4646
})
4747

48-
const result = await byteLengthFromUrl('https://example.com')
48+
const result = await byteLengthFromUrl({ url: 'https://example.com' })
4949
expect(result).toBe(1024)
5050
expect(fetch).toHaveBeenCalledWith('https://example.com', { method: 'HEAD' })
5151
})
5252

5353
it('throws an error if the response is not ok', async () => {
5454
global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, status: 404 })
5555

56-
await expect(byteLengthFromUrl('https://example.com')).rejects.toThrow('fetch head failed 404')
56+
await expect(byteLengthFromUrl({ url: 'https://example.com' })).rejects.toThrow('fetch head failed 404')
5757
})
5858

5959
it('falls back to GET with range if Content-Length header is missing from HEAD', async () => {
@@ -68,7 +68,7 @@ describe('byteLengthFromUrl', () => {
6868
headers: new Map([['Content-Range', 'bytes 0-0/2048']]),
6969
})
7070

71-
const result = await byteLengthFromUrl('https://example.com', undefined, customFetch)
71+
const result = await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
7272
expect(result).toBe(2048)
7373
expect(customFetch).toHaveBeenCalledTimes(2)
7474
})
@@ -85,11 +85,11 @@ describe('byteLengthFromUrl', () => {
8585

8686
})
8787

88-
const result = await byteLengthFromUrl('https://example.com', { headers: { Authorization: 'Bearer token' } } )
88+
const result = await byteLengthFromUrl({ url: 'https://example.com', requestInit: { headers: { Authorization: 'Bearer token' } } })
8989
expect(result).toBe(1024)
9090
expect(fetch).toHaveBeenCalledWith('https://example.com', { method: 'HEAD', headers: { Authorization: 'Bearer token' } })
9191

92-
await expect(byteLengthFromUrl('https://example.com')).rejects.toThrow('fetch head failed 401')
92+
await expect(byteLengthFromUrl({ url: 'https://example.com' })).rejects.toThrow('fetch head failed 401')
9393
})
9494

9595
it ('uses the provided fetch function, along with requestInit if passed', async () => {
@@ -99,7 +99,7 @@ describe('byteLengthFromUrl', () => {
9999
})
100100

101101
const requestInit = { headers: { authorization: 'Bearer token' } }
102-
const result = await byteLengthFromUrl('https://example.com', requestInit, customFetch)
102+
const result = await byteLengthFromUrl({ url: 'https://example.com', requestInit, fetch: customFetch })
103103
expect(result).toBe(2048)
104104
expect(customFetch).toHaveBeenCalledWith('https://example.com', { ...requestInit, method: 'HEAD' })
105105
})
@@ -113,7 +113,7 @@ describe('byteLengthFromUrl', () => {
113113
headers: new Map([['Content-Range', 'bytes 0-0/9446073']]),
114114
})
115115

116-
const result = await byteLengthFromUrl('https://example.com', undefined, customFetch)
116+
const result = await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
117117
expect(result).toBe(9446073)
118118
expect(customFetch).toHaveBeenCalledTimes(2)
119119
expect(customFetch).toHaveBeenNthCalledWith(1, 'https://example.com', { method: 'HEAD' })
@@ -128,7 +128,7 @@ describe('byteLengthFromUrl', () => {
128128
headers: new Map(),
129129
})
130130

131-
await expect(byteLengthFromUrl('https://example.com', undefined, customFetch)).rejects.toThrow('missing content-range header')
131+
await expect(byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })).rejects.toThrow('missing content-range header')
132132
})
133133

134134
it('fallback throws error if Content-Range header is invalid', async () => {
@@ -140,7 +140,7 @@ describe('byteLengthFromUrl', () => {
140140
headers: new Map([['Content-Range', 'invalid format']]),
141141
})
142142

143-
await expect(byteLengthFromUrl('https://example.com', undefined, customFetch)).rejects.toThrow('invalid content-range header')
143+
await expect(byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })).rejects.toThrow('invalid content-range header')
144144
})
145145

146146
it('fallback uses Content-Length when server returns 200 (Range not supported)', async () => {
@@ -155,7 +155,7 @@ describe('byteLengthFromUrl', () => {
155155
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
156156
})
157157

158-
const result = await byteLengthFromUrl('https://example.com', undefined, customFetch)
158+
const result = await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
159159
expect(result).toBe(5242880)
160160
})
161161

@@ -169,7 +169,7 @@ describe('byteLengthFromUrl', () => {
169169
body: null,
170170
})
171171

172-
await expect(byteLengthFromUrl('https://example.com', undefined, customFetch)).rejects.toThrow(
172+
await expect(byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })).rejects.toThrow(
173173
'server does not support range requests and missing content-length'
174174
)
175175
})
@@ -189,7 +189,7 @@ describe('byteLengthFromUrl', () => {
189189
})
190190
})
191191

192-
const result = await byteLengthFromUrl('https://example.com', undefined, customFetch)
192+
const result = await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
193193
expect(result).toBe(5242880)
194194
expect(capturedSignal).toBeDefined()
195195
// @ts-ignore - capturedSignal is assigned in the mock
@@ -210,7 +210,7 @@ describe('byteLengthFromUrl', () => {
210210
})
211211
})
212212

213-
const result = await byteLengthFromUrl('https://example.com', undefined, customFetch)
213+
const result = await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
214214
expect(result).toBe(9446073)
215215
expect(capturedSignal).toBeDefined()
216216
// @ts-ignore - capturedSignal is assigned in the mock
@@ -226,7 +226,7 @@ describe('byteLengthFromUrl', () => {
226226
headers: new Map([['Content-Range', 'bytes 0-0/1024']]),
227227
})
228228

229-
await byteLengthFromUrl('https://example.com', undefined, customFetch)
229+
await byteLengthFromUrl({ url: 'https://example.com', fetch: customFetch })
230230

231231
// Check second call (the GET with range)
232232
const secondCallArgs = customFetch.mock.calls[1]

0 commit comments

Comments
 (0)