fix(frontend): upload asset lolos proxy — BODY_SIZE_LIMIT 8M + proxy 413 jelas

Body upload (JSON base64, ~7 MiB untuk file 5 MiB) ditolak default
adapter-node 512K sebelum sampai backend Flask, lalu error itu
terlapor sebagai 502 'Backend tidak tersedia'.

- podman-compose: BODY_SIZE_LIMIT=8M untuk elemes-frontend
- hooks.server: SvelteKitError body-size (413) dipisah dari kegagalan
  koneksi — balas 413 'Ukuran file terlalu besar' bukan 502
- test: proxy balas 413 untuk body melebihi limit
This commit is contained in:
a2nr 2026-10-03 05:16:41 +07:00
parent c127309dd8
commit 8fd39d6e1a
3 changed files with 57 additions and 2 deletions

View File

@ -57,6 +57,39 @@ describe('hooks.server handle', () => {
expect(response.headers.get('Cross-Origin-Resource-Policy')).toBe('cross-origin');
});
it('should return 413 with size message when body exceeds adapter limit', async () => {
// SvelteKitError dari adapter-node saat body POST melebihi BODY_SIZE_LIMIT.
// Proxy harus balas 413 dengan pesan ukuran — bukan 502 "Backend tidak tersedia".
const arrayBufferMock = vi.fn().mockRejectedValue(
Object.assign(new Error('Content-length of 947488 exceeds limit of 524288 bytes.'), { status: 413 })
);
const mockEvent: any = {
request: {
method: 'POST',
headers: new Headers({ 'content-type': 'application/json' }),
arrayBuffer: arrayBufferMock
},
url: new URL('https://sinau-c-dev.manakin-gentoo.ts.net/api/content/assets/upload')
};
const origFetch = global.fetch;
global.fetch = vi.fn() as any;
try {
const resolve = vi.fn();
const response = await handle({ event: mockEvent, resolve });
expect(response.status).toBe(413);
const body = await response.json();
expect(body.success).toBe(false);
expect(body.message).toMatch(/terlalu besar/i);
// Tidak boleh dikirim ke backend sama sekali
expect(global.fetch).not.toHaveBeenCalled();
} finally {
global.fetch = origFetch;
}
});
it('should read body as arrayBuffer when proxying POST requests', async () => {
const arrayBufferMock = vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3]).buffer);
const mockEvent: any = {

View File

@ -77,9 +77,28 @@ export const handle: Handle = async ({ event, resolve }) => {
status: res.status,
headers: resHeaders,
});
} catch (err) {
} catch (err: any) {
// adapter-node melempar SvelteKitError saat body POST melebihi
// BODY_SIZE_LIMIT — bukan berarti backend mati. Laporkan sebagai 413
// dengan pesan yang jelas, jangan 502 "Backend tidak tersedia".
const isBodyTooLarge =
err?.status === 413 || /exceeds limit/i.test(String(err?.message ?? ''));
if (isBodyTooLarge) {
console.warn(`[hooks] Body too large for ${event.request.method} ${event.url.pathname}`);
return new Response(
JSON.stringify({
success: false,
error: 'Payload too large',
message: 'Ukuran file terlalu besar (maksimum 5 MiB per asset)'
}),
{
status: 413,
headers: { 'content-type': 'application/json' }
}
);
}
console.error(`API proxy error (${backendUrl}):`, err);
return new Response(JSON.stringify({ error: 'Backend unavailable' }), {
return new Response(JSON.stringify({ success: false, error: 'Backend unavailable', message: 'Backend tidak tersedia' }), {
status: 502,
headers: { 'content-type': 'application/json' },
});

View File

@ -83,6 +83,9 @@ services:
environment:
- ORIGIN=${ORIGIN:-http://localhost:3000}
- API_BACKEND=http://127.0.0.1:5000
# Backend menerima asset ≤ 5 MiB (JSON base64 ≈ 7 MiB di wire).
# Default adapter-node 512K menolak upload gambar sebelum sampai backend.
- BODY_SIZE_LIMIT=8M
- PUBLIC_APP_BAR_TITLE=${APP_BAR_TITLE}
- PUBLIC_COPYRIGHT_TEXT=${COPYRIGHT_TEXT}
- PUBLIC_PAGE_TITLE_SUFFIX=${PAGE_TITLE_SUFFIX}