Feat: Proxy image content through the API instead of redirecting to MinIO
Replace the presigned-URL redirect (302) in GET /api/v1/images/{id}/file
with a direct proxy that fetches bytes from S3 server-side and returns them
to the client. The browser never contacts the storage backend, eliminating
the /etc/hosts workaround needed in local development.
- StorageBackend: swap get_presigned_url for get(key) -> bytes
- S3StorageBackend: implement get() via aiobotocore get_object
- serve_image_file: return Response with ETag + Cache-Control: immutable
- test_serving: assert 200 + content-type + ETag; add no-storage-details test
- Spec Kit artifacts for feature 002-api-image-proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
62
specs/002-api-image-proxy/contracts/api.md
Normal file
62
specs/002-api-image-proxy/contracts/api.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# API Contract: Image Content Endpoint
|
||||
|
||||
**Branch**: `002-api-image-proxy` | **Date**: 2026-05-03
|
||||
|
||||
## Changed endpoint
|
||||
|
||||
### `GET /api/v1/images/{image_id}/file`
|
||||
|
||||
Serves the binary content of the image with the given ID, proxied through the API from object storage.
|
||||
|
||||
**Path parameters**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `image_id` | UUID | Unique identifier of the image |
|
||||
|
||||
**Responses**
|
||||
|
||||
#### `200 OK` — Image content
|
||||
|
||||
| Header | Value | Notes |
|
||||
|--------|-------|-------|
|
||||
| `Content-Type` | `image/jpeg` \| `image/png` \| `image/gif` \| `image/webp` | Taken from stored `mime_type` |
|
||||
| `ETag` | `"<sha256-hex>"` | SHA-256 hash of the image content; quoted string per RFC 7232 |
|
||||
| `Cache-Control` | `public, max-age=31536000, immutable` | Safe: images are immutable after upload |
|
||||
|
||||
Body: raw image bytes.
|
||||
|
||||
#### `404 Not Found` — Image not found
|
||||
|
||||
```json
|
||||
{ "detail": "Image not found", "code": "image_not_found" }
|
||||
```
|
||||
|
||||
#### `500 Internal Server Error` — Storage retrieval failure
|
||||
|
||||
```json
|
||||
{ "detail": "Failed to retrieve image content", "code": "storage_error" }
|
||||
```
|
||||
|
||||
No storage-specific details (bucket name, hostname, credentials) are exposed in error responses.
|
||||
|
||||
---
|
||||
|
||||
## Breaking change from prior behaviour
|
||||
|
||||
| Aspect | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Status code | `302 Found` | `200 OK` |
|
||||
| `Location` header | Present (presigned S3 URL) | Absent |
|
||||
| Body | Empty | Raw image bytes |
|
||||
| Browser behaviour | Follows redirect to storage | Receives content from API |
|
||||
|
||||
The endpoint path is unchanged. The UI requires no URL-construction changes.
|
||||
|
||||
---
|
||||
|
||||
## Removed StorageBackend capability
|
||||
|
||||
The `get_presigned_url` operation is removed from the `StorageBackend` interface. It was the only mechanism for generating time-limited direct-access URLs to stored objects, and had a single call site (`serve_image_file`). With the proxy in place it has no remaining callers.
|
||||
|
||||
Any future need for presigned URLs (e.g., direct-upload flows) would require a new spec and a new interface method.
|
||||
Reference in New Issue
Block a user