fix: download every selected book, not just the last
The frontend built book_ids with `set` rather than `append`, so the archive only ever held one book. The proxy also sent an empty Content-Length, which is not valid HTTP/1.1, and buffered the whole zip before forwarding a byte of it. Entries now carry the file's own name, suffixed when two books claim the same one, and the books are resolved before the stream starts so a miss is a 404 instead of a truncated archive under a 200.
This commit is contained in:
@@ -134,8 +134,11 @@ export class BookOperationsState {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local temporary, not reactive state
|
||||
const downloadUrl = new URL(`/api/books/download`, page.url.origin);
|
||||
downloadUrl.searchParams.set('library_id', this.libraryId);
|
||||
// `append`, not `set` — `set` replaces the existing value, so every book but
|
||||
// the last was dropped from the query string and the archive came back holding
|
||||
// one book however many were selected.
|
||||
bookIds.forEach((id) => {
|
||||
downloadUrl.searchParams.set('book_ids', id.toString());
|
||||
downloadUrl.searchParams.append('book_ids', id.toString());
|
||||
});
|
||||
|
||||
this.download(downloadUrl, filename || 'download.zip');
|
||||
|
||||
@@ -16,16 +16,27 @@ async function handleResponse(response: Response) {
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} else {
|
||||
// Handle file or other non-JSON response
|
||||
const responseBody = await response.arrayBuffer();
|
||||
// Only headers the backend actually sent are forwarded. Defaulting a missing
|
||||
// one to '' put `Content-Length: ` on the wire — SvelteKit's `setResponse`
|
||||
// hands every header to `res.setHeader` as-is, empty or not — and an empty
|
||||
// Content-Length is not valid HTTP/1.1, so strict clients reject the whole
|
||||
// response. The multi-book ZIP is streamed and carries no Content-Length,
|
||||
// which made it the one download that always tripped over this.
|
||||
const headers = new Headers({ 'Content-Type': contentType });
|
||||
for (const name of ['Content-Disposition', 'Content-Length']) {
|
||||
const value = response.headers.get(name);
|
||||
if (value) headers.set(name, value);
|
||||
}
|
||||
|
||||
return new Response(responseBody, {
|
||||
// The body is passed through as a stream, matching what `bodyOf` does for
|
||||
// requests. `arrayBuffer()` held the whole response here first, so nothing
|
||||
// reached the browser until the last byte had been read and this process
|
||||
// carried a copy of it — survivable for one book, not for a ZIP of a whole
|
||||
// shelf, which only starts arriving once the backend has finished
|
||||
// compressing all of it.
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': response.headers.get('Content-Disposition') || '',
|
||||
'Content-Length': response.headers.get('Content-Length') || ''
|
||||
}
|
||||
headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user