From caaa70115e662a136fc50184d8e9ee0fcbc49ec8 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 10 Aug 2026 20:15:38 +0900
Subject: [PATCH 1/2] test(api): require UTF-8 plain-text success response
---
.../EtlControllerUtf8ResponseTest.java | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java
new file mode 100644
index 00000000..9ad7cb26
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java
@@ -0,0 +1,45 @@
+package com.xtrmetl.etl.controller;
+
+import com.xtrmetl.etl.connector.ConnectorProperties;
+import com.xtrmetl.etl.connector.TargetConnectorDispatcher;
+import com.xtrmetl.etl.connector.TargetConnectorRegistry;
+import com.xtrmetl.etl.service.EtlService;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class EtlControllerUtf8ResponseTest {
+
+ @Test
+ void successfulUnicodeResultDeclaresUtf8TextRepresentation() throws Exception {
+ EtlService etlService = mock(EtlService.class);
+ TargetConnectorDispatcher dispatcher = new TargetConnectorDispatcher(
+ new TargetConnectorRegistry(),
+ new ConnectorProperties()
+ );
+ MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
+ new EtlController(etlService, dispatcher)
+ )
+ .setControllerAdvice(new EtlApiProblemHandler())
+ .build();
+
+ String request = "[{\"id\":\"레코드_α\"}]";
+ String response = "Processed: 레코드_α";
+ when(etlService.processData(request)).thenReturn(response);
+
+ mockMvc.perform(post("/api/etl/process")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(request))
+ .andExpect(status().isOk())
+ .andExpect(header().string("Content-Type", "text/plain;charset=UTF-8"))
+ .andExpect(content().string(response));
+ }
+}
From 026d408427f2ead6c1735f2837cae2f73786b54f Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 10 Aug 2026 20:17:50 +0900
Subject: [PATCH 2/2] fix(api): declare UTF-8 text success responses
---
.../xtrmetl/etl/controller/EtlController.java | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java
index 1f932d46..e0bfbdea 100644
--- a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java
+++ b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java
@@ -17,6 +17,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -35,6 +36,12 @@ public class EtlController {
/** Response header that tells a keyed client whether a prior success was replayed. */
public static final String IDEMPOTENCY_REPLAYED_HEADER = "Idempotency-Replayed";
+ private static final MediaType UTF8_TEXT_PLAIN = new MediaType(
+ "text",
+ "plain",
+ StandardCharsets.UTF_8
+ );
+
private final EtlService etlService;
private final TargetConnectorDispatcher connectorDispatcher;
@@ -62,14 +69,14 @@ public EtlController(EtlService etlService, TargetConnectorDispatcher connectorD
*
* The mapping does not constrain response negotiation to the successful representation.
* This allows callers that accept only {@code application/problem+json} to receive typed
- * failure responses before a successful text body is selected. Typed request and data-access
- * failures retain their dedicated handlers; only an unexpected runtime failure raised during
- * the service invocation is wrapped as an ETL internal failure.
+ * failure responses before a successful UTF-8 text body is selected. Typed request and
+ * data-access failures retain their dedicated handlers; only an unexpected runtime failure
+ * raised during the service invocation is wrapped as an ETL internal failure.
*
* @param jsonInput UTF-8 JSON array request body
* @param idempotencyKey optional bounded client-generated retry key
* @param principal authenticated caller namespace for keyed requests
- * @return existing newline-delimited plain-text success response, plus replay metadata when keyed
+ * @return newline-delimited UTF-8 plain-text success response, plus replay metadata when keyed
*/
@PostMapping("/process")
@Observed(name = "etl.process", contextualName = "etl-processing")
@@ -104,7 +111,7 @@ public ResponseEntity processData(
}
ResponseEntity.BodyBuilder response = ResponseEntity.ok()
- .contentType(MediaType.TEXT_PLAIN);
+ .contentType(UTF8_TEXT_PLAIN);
if (idempotencyKey != null) {
response.header(IDEMPOTENCY_REPLAYED_HEADER, Boolean.toString(replayed));
}
@@ -118,7 +125,7 @@ public ResponseEntity processData(
* direct controller tests and embedded integrations while delegating to the mapped method.
*
* @param jsonInput UTF-8 JSON array request body
- * @return existing newline-delimited plain-text success response
+ * @return newline-delimited UTF-8 plain-text success response
*/
public ResponseEntity processData(String jsonInput) {
return processData(jsonInput, null, null);