pass requestInit to fetch utils (#34)

* pass requestInit to fetch utils

It will allow authentication

* add tests
This commit is contained in:
Sylvain Lesage 2024-11-08 22:22:30 +01:00 committed by GitHub
parent 302ba655c3
commit 6ec836dac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 95 additions and 18 deletions

2
demo/bundle.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -78,7 +78,7 @@ export async function asyncBufferFrom(from: AsyncBufferFrom): Promise<AsyncBuffe
const key = JSON.stringify(from)
const cached = cache.get(key)
if (cached) return cached
const asyncBuffer = asyncBufferFromUrl(from.url, from.byteLength).then(cachedAsyncBuffer)
const asyncBuffer = asyncBufferFromUrl(from).then(cachedAsyncBuffer)
cache.set(key, asyncBuffer)
return asyncBuffer
} else {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -40,10 +40,11 @@ export function concat(aaa, bbb) {
* Get the byte length of a URL using a HEAD request.
*
* @param {string} url
* @param {RequestInit} [requestInit] fetch options
* @returns {Promise<number>}
*/
export async function byteLengthFromUrl(url) {
return await fetch(url, { method: 'HEAD' })
export async function byteLengthFromUrl(url, requestInit) {
return await fetch(url, { ...requestInit, method: 'HEAD' })
.then(res => {
if (!res.ok) throw new Error(`fetch head failed ${res.status}`)
const length = res.headers.get('Content-Length')
@ -56,21 +57,24 @@ export async function byteLengthFromUrl(url) {
* Construct an AsyncBuffer for a URL.
*
* @typedef {import('./types.js').AsyncBuffer} AsyncBuffer
* @param {string} url
* @param {number} [byteLength]
* @param {object} options
* @param {string} options.url
* @param {number} [options.byteLength]
* @param {RequestInit} [options.requestInit]
* @returns {Promise<AsyncBuffer>}
*/
export async function asyncBufferFromUrl(url, byteLength) {
export async function asyncBufferFromUrl({ url, byteLength, requestInit }) {
// byte length from HEAD request
byteLength ||= await byteLengthFromUrl(url)
byteLength ||= await byteLengthFromUrl(url, requestInit)
const init = requestInit || {}
return {
byteLength,
async slice(start, end) {
// fetch byte range from url
const headers = new Headers()
const headers = new Headers(init.headers)
const endStr = end === undefined ? '' : end - 1
headers.set('Range', `bytes=${start}-${endStr}`)
const res = await fetch(url, { headers })
const res = await fetch(url, { ...init, headers })
if (!res.ok || !res.body) throw new Error(`fetch failed ${res.status}`)
return res.arrayBuffer()
},

@ -67,6 +67,28 @@ describe('byteLengthFromUrl', () => {
await expect(byteLengthFromUrl('https://example.com')).rejects.toThrow('missing content length')
})
it ('passes authentication headers', async () => {
global.fetch = vi.fn().mockImplementation((url, options) => {
if (new Headers(options.headers).get('Authorization') !== 'Bearer token') {
return Promise.resolve({
ok: false,
status: 401,
})}
return Promise.resolve({
ok: true,
headers: new Map([['Content-Length', '1024']]),
})
})
const result = await byteLengthFromUrl('https://example.com', { headers: { Authorization: 'Bearer token' } } )
expect(result).toBe(1024)
expect(fetch).toHaveBeenCalledWith('https://example.com', { method: 'HEAD', headers: { Authorization: 'Bearer token' } })
await expect(byteLengthFromUrl('https://example.com')).rejects.toThrow('fetch head failed 401')
})
})
describe('asyncBufferFromUrl', () => {
@ -77,12 +99,12 @@ describe('asyncBufferFromUrl', () => {
headers: new Map([['Content-Length', '1024']]),
})
const buffer = await asyncBufferFromUrl('https://example.com')
const buffer = await asyncBufferFromUrl({ url: 'https://example.com' })
expect(buffer.byteLength).toBe(1024)
})
it('uses provided byte length if given', async () => {
const buffer = await asyncBufferFromUrl('https://example.com', 2048)
const buffer = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 2048 })
expect(buffer.byteLength).toBe(2048)
expect(fetch).toHaveBeenCalledOnce()
})
@ -95,7 +117,7 @@ describe('asyncBufferFromUrl', () => {
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
})
const buffer = await asyncBufferFromUrl('https://example.com', 1024)
const buffer = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 1024 })
const result = await buffer.slice(0, 100)
expect(result).toBe(mockArrayBuffer)
@ -112,7 +134,7 @@ describe('asyncBufferFromUrl', () => {
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
})
const buffer = await asyncBufferFromUrl('https://example.com', 1024)
const buffer = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 1024 })
await buffer.slice(100)
expect(fetch).toHaveBeenCalledWith('https://example.com', {
@ -126,7 +148,58 @@ describe('asyncBufferFromUrl', () => {
status: 404,
})
const buffer = await asyncBufferFromUrl('https://example.com', 1024)
const buffer = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 1024 })
await expect(buffer.slice(0, 100)).rejects.toThrow('fetch failed 404')
})
it('passes authentication headers to get the byteLength', async () => {
global.fetch = vi.fn().mockImplementation((url, options) => {
if (new Headers(options.headers).get('Authorization') !== 'Bearer token') {
return Promise.resolve({
ok: false,
status: 401,
})
}
return Promise.resolve({
ok: true,
headers: new Map([['Content-Length', '1024']]),
})
})
expect(asyncBufferFromUrl({ url: 'https://example.com' })).rejects.toThrow('fetch head failed 401')
const buffer = await asyncBufferFromUrl({ url: 'https://example.com', requestInit: { headers: { Authorization: 'Bearer token' } } } )
expect(buffer.byteLength).toBe(1024)
})
it ('passes authentication headers to fetch byte range', async () => {
const mockArrayBuffer = new ArrayBuffer(100)
global.fetch = vi.fn().mockImplementation((url, options) => {
if (new Headers(options.headers).get('Authorization') !== 'Bearer token') {
return Promise.resolve({
ok: false,
status: 401,
})
}
if (options.headers.get('Range') !== 'bytes=0-99') {
return Promise.resolve({
ok: false,
status: 404,
})
}
return Promise.resolve({
ok: true,
body: {},
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
})
})
const noHeaders = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 1024 })
expect(noHeaders.slice(0, 100)).rejects.toThrow('fetch failed 401')
const withHeaders = await asyncBufferFromUrl({ url: 'https://example.com', byteLength: 1024, requestInit: { headers: { Authorization: 'Bearer token' } } } )
expect (await withHeaders.slice(0, 100)).toBe(mockArrayBuffer)
expect (withHeaders.slice(0, 10)).rejects.toThrow('fetch failed 404')
})
})