feat: add custom HTTP metadata support for content delivery configs#3507
feat: add custom HTTP metadata support for content delivery configs#3507dievri wants to merge 1 commit intotolgee:mainfrom
Conversation
Allows configuring custom metadata (e.g. Cache-Control, Content-Type) per content delivery config, stored as jsonb. On S3/MinIO/GCS, well-known HTTP headers (Cache-Control, Content-Type, Content-Disposition, Content-Encoding, Content-Language) are mapped to proper PutObject fields so they appear as actual HTTP response headers; other keys go to x-amz-meta-*. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
📝 WalkthroughWalkthroughThis pull request adds custom metadata support for content delivery storage. The feature introduces an optional Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (3)
webapp/src/views/projects/developer/contentDelivery/CdDialog.tsx (1)
39-39: Use atg.viewsalias for the new import.This new relative import goes against the webapp import convention.
♻️ Suggested change
-import { CdCustomMetadata } from './CdCustomMetadata'; +import { CdCustomMetadata } from 'tg.views/projects/developer/contentDelivery/CdCustomMetadata';As per coding guidelines, "Use Tolgee custom TypeScript path aliases (tg.component, tg.service, tg.hooks, tg.views, tg.globalContext) instead of relative imports".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/views/projects/developer/contentDelivery/CdDialog.tsx` at line 39, The import of CdCustomMetadata in CdDialog.tsx uses a relative path; update the import to use the Tolgee views alias instead (use tg.views for CdCustomMetadata) so it follows the project's TypeScript path-alias convention; locate the import statement that references CdCustomMetadata and replace the relative import with the equivalent tg.views alias import.webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx (2)
2-2: Use atg.viewsalias here.Please avoid the relative import for
getCdEditInitialValuesand switch it to the Tolgee path alias so this new file stays consistent with the rest of the webapp import convention.As per coding guidelines,
webapp/**/*.{ts,tsx}: Use Tolgee custom TypeScript path aliases (tg.component, tg.service, tg.hooks, tg.views, tg.globalContext) instead of relative imports.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx` at line 2, The import uses a relative path for CdValues; replace the relative import of getCdEditInitialValues with the Tolgee path alias (tg.views) so CdValues is imported from "tg.views/getCdEditInitialValues" (or the project's equivalent tg.views module path) to conform with the webapp alias convention and keep CdCustomMetadata.tsx consistent with other imports.
27-51: Add stabledata-cyhooks to these new controls.The new inputs and add/remove actions currently expose no explicit E2E selectors, so tests will have to bind to translated labels or DOM structure.
As per coding guidelines,
**/*.{tsx,ts}: STRICTLY usedata-cyattributes for E2E selectors, never rely on text content; use typed helpersgcy()orcy.gcy().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx` around lines 27 - 51, Add stable data-cy attributes to the new custom metadata controls so E2E tests use selectors instead of labels or structure: add a data-cy to the key TextField (referenced as the TextField with name `customMetadata.${index}.key`), a data-cy to the value TextField (name `customMetadata.${index}.value`), a data-cy to the remove IconButton (the IconButton calling remove(index)), and a data-cy to the add Button (the Button calling push({ key: '', value: '' })). Use consistent, descriptive names (e.g. content-delivery-custom-metadata-key-<index>, -value-<index>, -remove-<index>, and -add) so tests can use gcy()/cy.gcy() to select them reliably.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/data/src/main/kotlin/io/tolgee/component/fileStorage/FileStorage.kt`:
- Around line 10-14: The FileStorage.storeFile signature currently accepts
metadata but some implementations (e.g., LocalFileStorage, AzureFileStorage)
silently ignore it; update the contract so callers get immediate feedback: add a
capability indicator (e.g., supportsMetadata(): Boolean) to the FileStorage
interface or change storeFile to throw when metadata is non-null for backends
that don't support it, then update implementations (LocalFileStorage,
AzureFileStorage) to either return false from supportsMetadata() or to throw
UnsupportedOperationException when metadata is passed; adjust any callers to
check supportsMetadata() before passing metadata or to handle the exception
accordingly.
In
`@backend/data/src/main/kotlin/io/tolgee/component/fileStorage/S3FileStorage.kt`:
- Around line 65-72: The metadata key handling in S3FileStorage.kt's loop (for
((key, value) in metadata)) only lowercases keys but doesn't trim whitespace, so
keys like "Content-Type " are misclassified; update the loop to first trim the
key (e.g., val trimmed = key.trim()) and use trimmed.lowercase() in the when
matcher and use the trimmed key when adding to customMetadata (so
customMetadata[trimmed] = value) to ensure header mapping works for keys with
extra whitespace.
In
`@backend/data/src/main/kotlin/io/tolgee/dtos/request/ContentDeliveryConfigRequest.kt`:
- Around line 53-58: The customMetadata map on ContentDeliveryConfigRequest
currently accepts arbitrary strings; add validation/normalization at the API
boundary by enforcing HTTP header token rules and disallowing surrounding
whitespace and CR/LF characters. Implement this by adding a validation step
(either a custom Jakarta Bean Validation constraint or a
validateCustomMetadata() method called from the controller/service) on
ContentDeliveryConfigRequest::customMetadata that: trims keys and values,
rejects empty keys/values, ensures keys match the HTTP token pattern (e.g.,
/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/) and values do not contain CR/LF or control
characters, and throw a BadRequestException (or ConstraintViolation) with a
clear message when validation fails; apply the same normalization/validation
path in the publishing/service logic that persists or forwards customMetadata to
storage to avoid bypassing the check.
In
`@backend/data/src/main/kotlin/io/tolgee/model/contentDelivery/ContentDeliveryConfig.kt`:
- Around line 147-150: The customMetadata property on ContentDeliveryConfig is
free-form and currently annotated with `@ActivityLoggedProp` which will persist
arbitrary values into audits; remove the `@ActivityLoggedProp` annotation from the
var customMetadata: Map<String, String>? declaration (or replace it with a
specific sanitizer wrapper) so arbitrary metadata values are not written to
activity logs, and if you still need visibility implement explicit sanitized
logging elsewhere (e.g., a method on ContentDeliveryConfig that returns a
redacted map of allowed keys) rather than annotating customMetadata directly.
In `@webapp/src/i18n/en.json`:
- Around line 780-783: Add the missing activity label for the
ContentDeliveryConfig.customMetadata field by adding the key
activity_entity_content_delivery_config.customMetadata to the en.json locale
with an appropriate human-readable value (e.g., "Custom metadata") so activity
logs use the translated label; locate the JSON entries near other
content_delivery_* keys (see ContentDeliveryConfig.customMetadata and
activity_entity_content_delivery_config.customMetadata) and insert the new
key/value following the project's localization style.
In `@webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx`:
- Around line 25-50: The custom metadata list allows duplicate keys which are
later collapsed by Object.fromEntries in useCdActions.tsx; add a validation step
in the CdCustomMetadata component (or the form submit handler that calls the
serializer) that checks values.customMetadata for duplicate keys by comparing
each entry's key.trim().toLowerCase(), and if any duplicates are found prevent
submit and surface a validation error on the offending rows (or a general form
error) so users must remove/rename duplicates before push/remove updates are
accepted; update the UI to show the error near the TextField inputs (the ones
using name={`customMetadata.${index}.key`}) so duplicates are fixed prior to
serialization.
In `@webapp/src/views/projects/developer/contentDelivery/CdDialog.tsx`:
- Around line 212-214: The submit path in useCdActions (the function that builds
payload via Object.fromEntries) currently collapses duplicate customMetadata
keys; add a pre-submit validation that inspects the CdCustomMetadata array
(customMetadata[].key) for duplicates, and if any duplicate keys are found,
prevent submission and surface a validation error to the user (e.g. set
form/dialog error or reject the submit promise) indicating which key(s) are
duplicated so the user can fix them; implement the duplicate check inside the
submit handler in useCdActions before Object.fromEntries is called and ensure
the error blocks the existing conversion and network call.
In `@webapp/src/views/projects/developer/contentDelivery/useCdActions.tsx`:
- Around line 60-67: The customMetadata payload builds entries from
values.customMetadata but uses untrimmed keys and can produce {} when all rows
are blank; update the logic that constructs customMetadata so it first filters
entries where e.key.trim() !== '' then maps to [e.key.trim(), e.value] (use the
trimmed key) and after Object.fromEntries check if the resulting object has any
keys—if not, set customMetadata to undefined; adjust code around
values.customMetadata and the customMetadata property to implement this change.
---
Nitpick comments:
In `@webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx`:
- Line 2: The import uses a relative path for CdValues; replace the relative
import of getCdEditInitialValues with the Tolgee path alias (tg.views) so
CdValues is imported from "tg.views/getCdEditInitialValues" (or the project's
equivalent tg.views module path) to conform with the webapp alias convention and
keep CdCustomMetadata.tsx consistent with other imports.
- Around line 27-51: Add stable data-cy attributes to the new custom metadata
controls so E2E tests use selectors instead of labels or structure: add a
data-cy to the key TextField (referenced as the TextField with name
`customMetadata.${index}.key`), a data-cy to the value TextField (name
`customMetadata.${index}.value`), a data-cy to the remove IconButton (the
IconButton calling remove(index)), and a data-cy to the add Button (the Button
calling push({ key: '', value: '' })). Use consistent, descriptive names (e.g.
content-delivery-custom-metadata-key-<index>, -value-<index>, -remove-<index>,
and -add) so tests can use gcy()/cy.gcy() to select them reliably.
In `@webapp/src/views/projects/developer/contentDelivery/CdDialog.tsx`:
- Line 39: The import of CdCustomMetadata in CdDialog.tsx uses a relative path;
update the import to use the Tolgee views alias instead (use tg.views for
CdCustomMetadata) so it follows the project's TypeScript path-alias convention;
locate the import statement that references CdCustomMetadata and replace the
relative import with the equivalent tg.views alias import.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 507c8f17-4cb8-4108-b31d-ea6411757053
📒 Files selected for processing (19)
backend/api/src/main/kotlin/io/tolgee/hateoas/contentDelivery/ContentDeliveryConfigModel.ktbackend/api/src/main/kotlin/io/tolgee/hateoas/contentDelivery/ContentDeliveryConfigModelAssembler.ktbackend/data/src/main/kotlin/io/tolgee/component/contentDelivery/ContentDeliveryFileStorageProvider.ktbackend/data/src/main/kotlin/io/tolgee/component/contentDelivery/ContentDeliveryUploader.ktbackend/data/src/main/kotlin/io/tolgee/component/fileStorage/AzureBlobFileStorage.ktbackend/data/src/main/kotlin/io/tolgee/component/fileStorage/FileStorage.ktbackend/data/src/main/kotlin/io/tolgee/component/fileStorage/LocalFileStorage.ktbackend/data/src/main/kotlin/io/tolgee/component/fileStorage/S3FileStorage.ktbackend/data/src/main/kotlin/io/tolgee/dtos/request/ContentDeliveryConfigRequest.ktbackend/data/src/main/kotlin/io/tolgee/model/contentDelivery/ContentDeliveryConfig.ktbackend/data/src/main/kotlin/io/tolgee/service/contentDelivery/ContentDeliveryConfigService.ktbackend/data/src/main/resources/db/changelog/schema.xmlbackend/development/src/main/kotlin/io/tolgee/util/InMemoryFileStorage.ktwebapp/src/i18n/en.jsonwebapp/src/service/apiSchema.generated.tswebapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsxwebapp/src/views/projects/developer/contentDelivery/CdDialog.tsxwebapp/src/views/projects/developer/contentDelivery/getCdEditInitialValues.tswebapp/src/views/projects/developer/contentDelivery/useCdActions.tsx
| fun storeFile( | ||
| storageFilePath: String, | ||
| bytes: ByteArray, | ||
| metadata: Map<String, String>? = null, | ||
| ) |
There was a problem hiding this comment.
Define behavior for backends that can't honor metadata.
The new contract makes metadata look universally supported, but the current Local/Azure implementations accept it and drop it. That means a config can save Cache-Control successfully and still publish objects without headers, with no feedback to the caller. Please make unsupported implementations fail fast or expose capability explicitly so callers can reject those configs up front.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/data/src/main/kotlin/io/tolgee/component/fileStorage/FileStorage.kt`
around lines 10 - 14, The FileStorage.storeFile signature currently accepts
metadata but some implementations (e.g., LocalFileStorage, AzureFileStorage)
silently ignore it; update the contract so callers get immediate feedback: add a
capability indicator (e.g., supportsMetadata(): Boolean) to the FileStorage
interface or change storeFile to throw when metadata is non-null for backends
that don't support it, then update implementations (LocalFileStorage,
AzureFileStorage) to either return false from supportsMetadata() or to throw
UnsupportedOperationException when metadata is passed; adjust any callers to
check supportsMetadata() before passing metadata or to handle the exception
accordingly.
| for ((key, value) in metadata) { | ||
| when (key.lowercase()) { | ||
| "cache-control" -> b.cacheControl(value) | ||
| "content-type" -> b.contentType(value) | ||
| "content-disposition" -> b.contentDisposition(value) | ||
| "content-encoding" -> b.contentEncoding(value) | ||
| "content-language" -> b.contentLanguage(value) | ||
| else -> customMetadata[key] = value |
There was a problem hiding this comment.
Trim metadata keys before header mapping.
The frontend currently forwards the raw key text, and this matcher only lowercases it. A user-entered "Content-Type " or " Cache-Control" will miss the standard-header branch and be uploaded as custom metadata instead of an HTTP response header, which defeats the main feature.
Proposed fix
- for ((key, value) in metadata) {
- when (key.lowercase()) {
+ for ((rawKey, value) in metadata) {
+ val key = rawKey.trim()
+ if (key.isEmpty()) {
+ continue
+ }
+ when (key.lowercase()) {
"cache-control" -> b.cacheControl(value)
"content-type" -> b.contentType(value)
"content-disposition" -> b.contentDisposition(value)
"content-encoding" -> b.contentEncoding(value)
"content-language" -> b.contentLanguage(value)
else -> customMetadata[key] = value
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for ((key, value) in metadata) { | |
| when (key.lowercase()) { | |
| "cache-control" -> b.cacheControl(value) | |
| "content-type" -> b.contentType(value) | |
| "content-disposition" -> b.contentDisposition(value) | |
| "content-encoding" -> b.contentEncoding(value) | |
| "content-language" -> b.contentLanguage(value) | |
| else -> customMetadata[key] = value | |
| for ((rawKey, value) in metadata) { | |
| val key = rawKey.trim() | |
| if (key.isEmpty()) { | |
| continue | |
| } | |
| when (key.lowercase()) { | |
| "cache-control" -> b.cacheControl(value) | |
| "content-type" -> b.contentType(value) | |
| "content-disposition" -> b.contentDisposition(value) | |
| "content-encoding" -> b.contentEncoding(value) | |
| "content-language" -> b.contentLanguage(value) | |
| else -> customMetadata[key] = value | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/data/src/main/kotlin/io/tolgee/component/fileStorage/S3FileStorage.kt`
around lines 65 - 72, The metadata key handling in S3FileStorage.kt's loop (for
((key, value) in metadata)) only lowercases keys but doesn't trim whitespace, so
keys like "Content-Type " are misclassified; update the loop to first trim the
key (e.g., val trimmed = key.trim()) and use trimmed.lowercase() in the when
matcher and use the trimmed key when adding to customMetadata (so
customMetadata[trimmed] = value) to ensure header mapping works for keys with
extra whitespace.
| @Schema( | ||
| description = | ||
| "Custom metadata to set on uploaded storage objects (e.g. {\"Cache-Control\": \"max-age=3600\"}). " + | ||
| "Currently supported for S3-compatible storages only.", | ||
| ) | ||
| var customMetadata: Map<String, String>? = null |
There was a problem hiding this comment.
Validate metadata keys and values at the API boundary.
This accepts arbitrary header names and values today. Invalid tokens, surrounding whitespace, or CR/LF characters won't be caught until publish time, and some of these values can become real response headers on the CDN. Please add backend validation/normalization here (or in the service) so bad metadata is rejected before it is stored.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/data/src/main/kotlin/io/tolgee/dtos/request/ContentDeliveryConfigRequest.kt`
around lines 53 - 58, The customMetadata map on ContentDeliveryConfigRequest
currently accepts arbitrary strings; add validation/normalization at the API
boundary by enforcing HTTP header token rules and disallowing surrounding
whitespace and CR/LF characters. Implement this by adding a validation step
(either a custom Jakarta Bean Validation constraint or a
validateCustomMetadata() method called from the controller/service) on
ContentDeliveryConfigRequest::customMetadata that: trims keys and values,
rejects empty keys/values, ensures keys match the HTTP token pattern (e.g.,
/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/) and values do not contain CR/LF or control
characters, and throw a BadRequestException (or ConstraintViolation) with a
clear message when validation fails; apply the same normalization/validation
path in the publishing/service logic that persists or forwards customMetadata to
storage to avoid bypassing the check.
| @Type(JsonBinaryType::class) | ||
| @Column(columnDefinition = "jsonb") | ||
| @ActivityLoggedProp | ||
| var customMetadata: Map<String, String>? = null |
There was a problem hiding this comment.
Don't activity-log arbitrary metadata values.
customMetadata is a free-form map, so @ActivityLoggedProp will persist every value into the audit trail. That can leak sensitive data if someone stores headers like Authorization, signed cookies, or vendor-specific tokens here. Please either ignore this field in activity logs or log only sanitized keys.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/data/src/main/kotlin/io/tolgee/model/contentDelivery/ContentDeliveryConfig.kt`
around lines 147 - 150, The customMetadata property on ContentDeliveryConfig is
free-form and currently annotated with `@ActivityLoggedProp` which will persist
arbitrary values into audits; remove the `@ActivityLoggedProp` annotation from the
var customMetadata: Map<String, String>? declaration (or replace it with a
specific sanitizer wrapper) so arbitrary metadata values are not written to
activity logs, and if you still need visibility implement explicit sanitized
logging elsewhere (e.g., a method on ContentDeliveryConfig that returns a
redacted map of allowed keys) rather than annotating customMetadata directly.
| "content_delivery_custom_metadata_label": "Custom object metadata", | ||
| "content_delivery_custom_metadata_key": "Key", | ||
| "content_delivery_custom_metadata_value": "Value", | ||
| "content_delivery_custom_metadata_add": "Add metadata entry", |
There was a problem hiding this comment.
Add the activity label for customMetadata.
ContentDeliveryConfig.customMetadata is activity-logged in backend/model/src/main/kotlin/io/tolgee/model/contentDelivery/ContentDeliveryConfig.kt:147-150, but this locale file still lacks activity_entity_content_delivery_config.customMetadata. Without it, update history for this field will fall back to a raw/untranslated property name.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/i18n/en.json` around lines 780 - 783, Add the missing activity
label for the ContentDeliveryConfig.customMetadata field by adding the key
activity_entity_content_delivery_config.customMetadata to the en.json locale
with an appropriate human-readable value (e.g., "Custom metadata") so activity
logs use the translated label; locate the JSON entries near other
content_delivery_* keys (see ContentDeliveryConfig.customMetadata and
activity_entity_content_delivery_config.customMetadata) and insert the new
key/value following the project's localization style.
| {values.customMetadata.map((_, index) => ( | ||
| <StyledRow key={index}> | ||
| <TextField | ||
| size="small" | ||
| name={`customMetadata.${index}.key`} | ||
| label={<T keyName="content_delivery_custom_metadata_key" />} | ||
| minHeight={false} | ||
| /> | ||
| <TextField | ||
| size="small" | ||
| name={`customMetadata.${index}.value`} | ||
| label={<T keyName="content_delivery_custom_metadata_value" />} | ||
| minHeight={false} | ||
| /> | ||
| <IconButton size="small" onClick={() => remove(index)}> | ||
| <Trash01 width={16} height={16} /> | ||
| </IconButton> | ||
| </StyledRow> | ||
| ))} | ||
| <Box> | ||
| <Button | ||
| size="small" | ||
| startIcon={<Plus width={16} height={16} />} | ||
| onClick={() => push({ key: '', value: '' })} | ||
| > | ||
| <T keyName="content_delivery_custom_metadata_add" /> |
There was a problem hiding this comment.
Validate duplicate metadata keys before submit.
webapp/src/views/projects/developer/contentDelivery/useCdActions.tsx:56-60 serializes this array with Object.fromEntries(...), so two rows with the same header name are silently collapsed and only the last value is persisted. Please enforce uniqueness here (ideally case-insensitive after trim), otherwise the saved config can differ from what the dialog shows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/views/projects/developer/contentDelivery/CdCustomMetadata.tsx`
around lines 25 - 50, The custom metadata list allows duplicate keys which are
later collapsed by Object.fromEntries in useCdActions.tsx; add a validation step
in the CdCustomMetadata component (or the form submit handler that calls the
serializer) that checks values.customMetadata for duplicate keys by comparing
each entry's key.trim().toLowerCase(), and if any duplicates are found prevent
submit and surface a validation error on the offending rows (or a general form
error) so users must remove/rename duplicates before push/remove updates are
accepted; update the UI to show the error near the TextField inputs (the ones
using name={`customMetadata.${index}.key`}) so duplicates are fixed prior to
serialization.
| <Box sx={{ gridColumn: '1 / span 2' }}> | ||
| <CdCustomMetadata /> | ||
| </Box> |
There was a problem hiding this comment.
Reject duplicate metadata keys before submit.
The submit path in webapp/src/views/projects/developer/contentDelivery/useCdActions.tsx:60-67 converts the entries with Object.fromEntries(...), so duplicate keys are silently collapsed and only the last value is sent. Now that this editor is exposed in the dialog, add uniqueness validation for customMetadata[].key to avoid silent data loss.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/views/projects/developer/contentDelivery/CdDialog.tsx` around
lines 212 - 214, The submit path in useCdActions (the function that builds
payload via Object.fromEntries) currently collapses duplicate customMetadata
keys; add a pre-submit validation that inspects the CdCustomMetadata array
(customMetadata[].key) for duplicates, and if any duplicate keys are found,
prevent submission and surface a validation error to the user (e.g. set
form/dialog error or reject the submit promise) indicating which key(s) are
duplicated so the user can fix them; implement the duplicate check inside the
submit handler in useCdActions before Object.fromEntries is called and ensure
the error blocks the existing conversion and network call.
| customMetadata: | ||
| values.customMetadata.length > 0 | ||
| ? Object.fromEntries( | ||
| values.customMetadata | ||
| .filter((e) => e.key.trim() !== '') | ||
| .map((e) => [e.key, e.value]) | ||
| ) | ||
| : undefined, |
There was a problem hiding this comment.
Normalize filtered keys before serializing metadata.
Blank rows are filtered with trim(), but the payload still uses the untrimmed key. A value like " Cache-Control " will miss the backend's special-header mapping, and rows that become empty after trimming currently serialize as {} instead of undefined.
♻️ Proposed fix
- customMetadata:
- values.customMetadata.length > 0
- ? Object.fromEntries(
- values.customMetadata
- .filter((e) => e.key.trim() !== '')
- .map((e) => [e.key, e.value])
- )
- : undefined,
+ customMetadata: (() => {
+ const entries = values.customMetadata
+ .map(({ key, value }) => [key.trim(), value] as const)
+ .filter(([key]) => key !== '');
+
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
+ })(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/views/projects/developer/contentDelivery/useCdActions.tsx` around
lines 60 - 67, The customMetadata payload builds entries from
values.customMetadata but uses untrimmed keys and can produce {} when all rows
are blank; update the logic that constructs customMetadata so it first filters
entries where e.key.trim() !== '' then maps to [e.key.trim(), e.value] (use the
trimmed key) and after Object.fromEntries check if the resulting object has any
keys—if not, set customMetadata to undefined; adjust code around
values.customMetadata and the customMetadata property to implement this change.
JanCizmar
left a comment
There was a problem hiding this comment.
Hello! Thanks a lot for the PR ❤️
Would you be open to add some tests (backend integration + E2E)?
| val customMetadata = mutableMapOf<String, String>() | ||
| for ((key, value) in metadata) { | ||
| when (key.lowercase()) { | ||
| "cache-control" -> b.cacheControl(value) |
There was a problem hiding this comment.
I wonder, why we cannot treat all the metadata the same. Why do we need this special cases?
Related: #2504, #3170
What this does
Adds support for configuring custom HTTP metadata (e.g.
Cache-Control,Content-Type) per content delivery config. The metadata is set on every S3/GCSPutObjectcall when publishing translation files.Motivation
Two practical problems this solves:
Cache-Controlon published objects, so CDNs cannot be instructed how to cache them per delivery configapplication/octet-streamby default, so CDNs (e.g. Google Cloud CDN) skip gzip/brotli compression. SettingContent-Type: application/jsonvia this feature fixes thatChanges
Backend
customMetadatajsonb column oncontent_delivery_config(Liquibase migration included)FileStorage.storeFile()interface extended with optionalmetadataparameterS3FileStorage: well-known HTTP headers (Cache-Control,Content-Type,Content-Disposition,Content-Encoding,Content-Language) are routed to their properPutObjectRequestbuilder fields — so they appear as actual HTTP response headers, not asx-amz-meta-*Frontend
Test plan
Cache-Control: public, max-age=300to a content delivery config, publish, verify the header appears on the object (not asx-amz-meta-cache-control)Content-Type: application/json, verify CDN serves it with correct content type and compresses itSummary by CodeRabbit