Skip to content

Commit 6e3b560

Browse files
authored
Made max header length configurable for cloudevents. (#4)
* Made max header length configurable for cloudevents. * Updated to 4k as the default header length.
1 parent 9b41b18 commit 6e3b560

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed

eoapi_notifier/outputs/cloudevents.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class CloudEventsConfig(BasePluginConfig):
2828
timeout: float = 30.0
2929
max_retries: int = 3
3030
retry_backoff: float = 1.0
31+
max_header_length: int = 4096
3132

3233
@field_validator("endpoint")
3334
@classmethod
@@ -53,6 +54,7 @@ def get_sample_config(cls) -> dict[str, Any]:
5354
"timeout": 30.0,
5455
"max_retries": 3,
5556
"retry_backoff": 1.0,
57+
"max_header_length": 4096,
5658
}
5759

5860
@classmethod
@@ -191,6 +193,16 @@ async def send_event(self, event: NotificationEvent) -> bool:
191193
)
192194
return False
193195

196+
def _truncate_header(self, value: str | None) -> str | None:
197+
"""Truncate header value to max_header_length if needed."""
198+
if not value:
199+
return value
200+
if len(value.encode("utf-8")) <= self.config.max_header_length:
201+
return value
202+
# Truncate to byte limit, ensuring valid UTF-8
203+
truncated = value.encode("utf-8")[: self.config.max_header_length]
204+
return truncated.decode("utf-8", errors="ignore")
205+
194206
def _convert_to_cloudevent(self, event: NotificationEvent) -> CloudEvent:
195207
"""Convert NotificationEvent to CloudEvent."""
196208
# Use config values which now include environment overrides
@@ -211,11 +223,15 @@ def _convert_to_cloudevent(self, event: NotificationEvent) -> CloudEvent:
211223

212224
# Add subject if item_id exists
213225
if event.item_id:
214-
attributes["subject"] = event.item_id
226+
truncated_subject = self._truncate_header(event.item_id)
227+
if truncated_subject:
228+
attributes["subject"] = truncated_subject
215229

216230
# Add collection attribute
217231
if event.collection:
218-
attributes["collection"] = event.collection
232+
truncated_collection = self._truncate_header(event.collection)
233+
if truncated_collection:
234+
attributes["collection"] = truncated_collection
219235

220236
# Event data payload
221237
data = {

examples/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ outputs:
6161
# Optional: HTTP settings
6262
# timeout: 30.0 # CLOUDEVENTS_TIMEOUT
6363
# max_retries: 3 # CLOUDEVENTS_MAX_RETRIES
64+
# max_header_length: 4096 # CLOUDEVENTS_MAX_HEADER_LENGTH
6465

6566
# Example with multiple sources and outputs
6667
# sources:

tests/test_cloudevents_output.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def test_default_configuration(self) -> None:
3434
assert config.event_type == "org.eoapi.stac"
3535
assert config.timeout == 30.0
3636
assert config.max_retries == 3
37+
assert config.max_header_length == 4096
3738

3839
def test_endpoint_validation_error(self) -> None:
3940
"""Test endpoint validation."""
@@ -198,6 +199,62 @@ def test_convert_to_cloudevent(
198199
assert cloud_event["subject"] == "test-item"
199200
assert cloud_event["collection"] == "test-collection"
200201

202+
def test_truncate_header(self, adapter: CloudEventsAdapter) -> None:
203+
"""Test header value truncation."""
204+
# Short string should not be truncated
205+
short = "short-string"
206+
assert adapter._truncate_header(short) == short
207+
208+
# None should remain None
209+
assert adapter._truncate_header(None) is None
210+
211+
# Long string should be truncated to max_header_length bytes
212+
long_string = "a" * 3000
213+
truncated = adapter._truncate_header(long_string)
214+
assert truncated is not None
215+
assert len(truncated.encode("utf-8")) <= adapter.config.max_header_length
216+
assert len(truncated) <= adapter.config.max_header_length
217+
218+
# UTF-8 multi-byte characters should be handled correctly
219+
unicode_string = "测试" * 1000 # Chinese characters (3 bytes each)
220+
truncated_unicode = adapter._truncate_header(unicode_string)
221+
assert truncated_unicode is not None
222+
assert (
223+
len(truncated_unicode.encode("utf-8")) <= adapter.config.max_header_length
224+
)
225+
# Should not break in the middle of a character
226+
assert truncated_unicode.encode("utf-8").decode("utf-8") == truncated_unicode
227+
228+
def test_convert_to_cloudevent_with_long_headers(
229+
self, config: CloudEventsConfig
230+
) -> None:
231+
"""Test CloudEvent conversion with long header values."""
232+
config.max_header_length = 50 # Small limit for testing
233+
adapter = CloudEventsAdapter(config)
234+
235+
# Create event with long item_id and collection
236+
event = NotificationEvent(
237+
source="/test/source",
238+
type="test.type",
239+
operation="INSERT",
240+
collection="a-very-long-collection-name-that-exceeds-the-limit",
241+
item_id="a-very-long-item-id-that-also-exceeds-the-configured-limit",
242+
)
243+
244+
cloud_event = adapter._convert_to_cloudevent(event)
245+
246+
# Check that long values are truncated in headers
247+
assert "subject" in cloud_event
248+
assert "collection" in cloud_event
249+
assert len(cloud_event["subject"].encode("utf-8")) <= config.max_header_length
250+
assert (
251+
len(cloud_event["collection"].encode("utf-8")) <= config.max_header_length
252+
)
253+
254+
# Original values should still be in data payload
255+
assert cloud_event.data["item_id"] == event.item_id
256+
assert cloud_event.data["collection"] == event.collection
257+
201258
def test_operation_mapping(self, adapter: CloudEventsAdapter) -> None:
202259
"""Test operation to event type mapping."""
203260
test_cases = [

0 commit comments

Comments
 (0)