Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/dedupeRequestHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ export type DedupeHashParams = {
request?: Request
}

function toBase64(bytes: Uint8Array): string {
// Use Node Buffer when available, otherwise fall back to btoa for browser-like runtimes.
const maybeBuffer = (
globalThis as {
Buffer?: {
from(input: Uint8Array): { toString(encoding: string): string }
}
}
).Buffer
if (maybeBuffer) {
return maybeBuffer.from(bytes).toString('base64')
}

let binary = ''
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
if (typeof btoa === 'function') {
return btoa(binary)
}

throw new Error('Base64 encoding is not available in this runtime')
}

export function dedupeRequestHash(
params: DedupeHashParams
): string | undefined {
Expand All @@ -34,9 +58,9 @@ export function dedupeRequestHash(
} else if (body instanceof URLSearchParams) {
bodyString = body.toString()
} else if (body instanceof ArrayBuffer) {
bodyString = Buffer.from(body).toString('base64')
bodyString = toBase64(new Uint8Array(body))
} else if (body instanceof Uint8Array) {
bodyString = Buffer.from(body).toString('base64')
bodyString = toBase64(body)
} else if (body instanceof Blob) {
bodyString = `[blob:${body.type}:${body.size}]`
} else if (body == null) {
Expand Down
42 changes: 41 additions & 1 deletion test/plugins/dedupeRequestHash.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, afterEach } from 'vitest'
import {
dedupeRequestHash,
DedupeHashParams,
} from '../../src/dedupeRequestHash'

describe('dedupeRequestHash', () => {
const originalBuffer = (globalThis as { Buffer?: unknown }).Buffer
const originalBtoa = (globalThis as { btoa?: unknown }).btoa

afterEach(() => {
;(globalThis as { Buffer?: unknown }).Buffer = originalBuffer
;(globalThis as { btoa?: unknown }).btoa = originalBtoa
})

it('returns undefined for FormData body', () => {
const form = new FormData()
form.append('foo', 'bar')
Expand Down Expand Up @@ -100,4 +108,36 @@ describe('dedupeRequestHash', () => {
const hash = dedupeRequestHash(params)
expect(hash).toContain('[unserializable-body]')
})

it('falls back to btoa when Buffer is unavailable', () => {
;(globalThis as { Buffer?: unknown }).Buffer = undefined
;(globalThis as { btoa?: (input: string) => string }).btoa = (input) => {
if (input === 'abc') return 'YWJj'
throw new Error('unexpected btoa input')
}

const buf = new Uint8Array([97, 98, 99]).buffer
const params: DedupeHashParams = {
method: 'PUT',
url: 'https://example.com',
body: buf,
}

expect(dedupeRequestHash(params)).toBe('PUT|https://example.com|YWJj')
})

it('throws when neither Buffer nor btoa are available', () => {
;(globalThis as { Buffer?: unknown }).Buffer = undefined
;(globalThis as { btoa?: unknown }).btoa = undefined

const params: DedupeHashParams = {
method: 'PATCH',
url: 'https://example.com',
body: new Uint8Array([1, 2, 3]),
}

expect(() => dedupeRequestHash(params)).toThrow(
'Base64 encoding is not available in this runtime'
)
})
})