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
5 changes: 5 additions & 0 deletions .changeset/veria-308-redirect-credential-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mppx': patch
---

Retried payment credentials against the final challenge response URL.
29 changes: 29 additions & 0 deletions src/client/internal/Fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,35 @@ describe('Fetch.from: 402 retry path', () => {
expect(headers.Authorization).toBe('credential')
})

test('sends credential retry to the final 402 response URL', async () => {
let callCount = 0
const calls: { input: RequestInfo | URL; init: RequestInit | undefined }[] = []
const mockFetch: typeof globalThis.fetch = async (input, init) => {
calls.push({ input, init })
callCount++
if (callCount === 1) {
const response = make402()
Object.defineProperty(response, 'url', {
value: 'https://payments.example.com/protected',
})
return response
}
return new Response('OK', { status: 200 })
}

const fetch = Fetch.from({
fetch: mockFetch,
methods: [noopMethod],
})

const response = await fetch('https://api.example.com/protected')

expect(response.status).toBe(200)
expect(calls[0]!.input).toBe('https://api.example.com/protected')
expect(calls[1]!.input).toBe('https://payments.example.com/protected')
expect(new Headers(calls[1]!.init?.headers).get('Authorization')).toBe('credential')
})

test('emits client events and allows challenge handler to provide credential', async () => {
const events: string[] = []
const createCredential = vi.fn(async () => 'method-credential')
Expand Down
18 changes: 14 additions & 4 deletions src/client/internal/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,13 @@ export function from<const methods extends readonly Method.AnyClient[]>(
}),
)

const paymentResponse = await baseFetch(initialRequest.input, {
...fetchInit,
headers: withAuthorizationHeader(initialRequest.headers, credential),
})
const paymentResponse = await baseFetch(
resolvePaymentRetryInput(response, initialRequest.input),
{
...fetchInit,
headers: withAuthorizationHeader(initialRequest.headers, credential),
},
)
if (paymentResponse.ok)
await events.emit(
'payment.response',
Expand Down Expand Up @@ -843,3 +846,10 @@ function resolveRequestUrl(input: RequestInfo | URL): URL {
if (input instanceof Request) return new URL(input.url)
return new URL(input, isBrowser() ? globalThis.location.href : undefined)
}

function resolvePaymentRetryInput(
response: Response,
fallback: RequestInfo | URL,
): RequestInfo | URL {
return response.url ? response.url : fallback
}
Loading