Ktor 2.3 ContentNegotiation plugin overriding user‑specified Content‑Type header
0 reputation · 29 May 2022, 15:13 UTC
0 reputation · 29 May 2022, 15:13 UTC
When using Ktor’s ContentNegotiation plugin to serialize a request body, the goal is to have any manually supplied Content-Type header remain unchanged in the outgoing request.
However, the plugin’s documentation only states that it adds the Content-Type header when it is missing, leaving the precedence rule undefined when a header is already present. Observations show that Ktor 2.2 preserves the user‑provided header, while Ktor 2.3 appears to override it, which may cause downstream services to reject the payload.
Does the ContentNegotiation plugin in Ktor 2.3 override a user‑provided Content‑Type header when serializing a request body? If it does, under what conditions does it preserve the header? What configuration or plugin ordering can be used to guarantee header preservation across Ktor versions?
34775 reputation · 30 May 2022, 01:55 UTC
Yes. In Ktor 2.3 the ContentNegotiation plugin unconditionally sets the Content-Type header based on the selected serializer, overwriting any header that the application may have already added. In Ktor 2.2 the plugin only added the header when it was missing, so a user‑supplied header was preserved.
call.request.headers[HttpHeaders.Content-Type] = contentType regardless of whether the header already exists.null or missing header, leaving an existing header untouched.Content-Type to reject the payload when the serializer’s type differs from the user‑provided value.The modification was probably introduced to simplify content‑type handling inside the plugin, but it broke the previous “add‑only‑if‑missing” contract and thus the backward‑compatible behavior.
call.request.headers.append(HttpHeaders.Content-Type, "application/vnd.my+json") (or equivalent) prior to the plugin installation ensures the value is not replaced.ApplicationCallPipeline.Call) that copies the desired header back onto the request:install(ContentNegotiation) {
json()
}
intercept(ApplicationCallPipeline.Call) {
// re‑apply the user‑provided Content‑Type if needed
val desired = "application/vnd.my+json"
call.request.headers[HttpHeaders.Content-Type] = desired
}
ContentTypeResolver that returns null for the produced type, then set the header yourself. This is more involved and generally unnecessary if you can control installation order.Are you using ContentNegotiation for request serialization (produce) or only for response consumption (consume)? If you are only consuming responses, the outgoing Content-Type header is irrelevant and the override does not affect your case.
Use comments to ask for clarification. Post a solution as an answer.
5,940 reputation · 29 May 2022, 22:33 UTC
When investigating header overrides in Ktor 2.3, it is important to distinguish between the ContentNegotiation plugin's logic and the final header merging performed by the HTTP engine. Because the plugin intercepts the send pipeline to transform the body, it may call setContentType() based on the selected converter, which can overwrite existing values depending on the internal pipeline phase.
To confirm if the override is happening at the plugin level or the engine level, you can use the MockEngine to inspect the HttpRequestData before it leaves the client:
val client = HttpClient(MockEngine) {
install(ContentNegotiation) { json() }
}
// Manually set header and observe if it persists in the MockEngine request
If the header is being overwritten, verifying the installation order of other plugins (such as custom interceptors) is critical, as later phases in the pipeline can modify headers after ContentNegotiation has executed.