diff --git a/apps/api/src/books.rs b/apps/api/src/books.rs index 22960ff..93d4c1e 100644 --- a/apps/api/src/books.rs +++ b/apps/api/src/books.rs @@ -631,12 +631,17 @@ pub async fn get_thumbnail( crate::pages::render_book_page_1(&state, book_id, 300, 80).await? }; + let etag_value = format!("\"{}_{:x}\"", book_id, data.len()); + let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type)); headers.insert( header::CACHE_CONTROL, HeaderValue::from_static("public, max-age=31536000, immutable"), ); + if let Ok(v) = HeaderValue::from_str(&etag_value) { + headers.insert(header::ETAG, v); + } Ok((StatusCode::OK, headers, Body::from(data))) } diff --git a/apps/backoffice/app/api/books/[bookId]/thumbnail/route.ts b/apps/backoffice/app/api/books/[bookId]/thumbnail/route.ts index 55e6185..b5b0863 100644 --- a/apps/backoffice/app/api/books/[bookId]/thumbnail/route.ts +++ b/apps/backoffice/app/api/books/[bookId]/thumbnail/route.ts @@ -9,10 +9,25 @@ export async function GET( try { const { baseUrl, token } = config(); + const ifNoneMatch = request.headers.get("if-none-match"); + + const fetchHeaders: Record = { + Authorization: `Bearer ${token}`, + }; + if (ifNoneMatch) { + fetchHeaders["If-None-Match"] = ifNoneMatch; + } + const response = await fetch(`${baseUrl}/books/${bookId}/thumbnail`, { - headers: { Authorization: `Bearer ${token}` }, + headers: fetchHeaders, + next: { revalidate: 86400 }, }); + // Forward 304 Not Modified as-is + if (response.status === 304) { + return new NextResponse(null, { status: 304 }); + } + if (!response.ok) { return new NextResponse(`Failed to fetch thumbnail: ${response.status}`, { status: response.status @@ -20,13 +35,17 @@ export async function GET( } const contentType = response.headers.get("content-type") || "image/webp"; + const etag = response.headers.get("etag"); - return new NextResponse(response.body, { - headers: { - "Content-Type": contentType, - "Cache-Control": "public, max-age=31536000, immutable", - }, - }); + const headers: Record = { + "Content-Type": contentType, + "Cache-Control": "public, max-age=31536000, immutable", + }; + if (etag) { + headers["ETag"] = etag; + } + + return new NextResponse(response.body, { headers }); } catch (error) { console.error("Error fetching thumbnail:", error); return new NextResponse("Failed to fetch thumbnail", { status: 500 });