akka / akka-http

The Streaming-first HTTP server/module of Akka
https://doc.akka.io/libraries/akka-http/current/
Other
1.34k stars 594 forks source link

multipart.bodypart cannot customize 'Content-Type' header #2792

Closed Hugh-ifly closed 4 years ago

Hugh-ifly commented 5 years ago

My target is to implement an http2 server which is compatible with Amazon Alexa-Voice-Service protocols ( https://developer.amazon.com/docs/alexa-voice-service/structure-http2-request.html#responses).

The goal is to construct a Multipart-related HttpResponse. Here is an Example of HttpResponse as below:

:status = 200
content-type = multipart/related; boundary=this-is-a-boundary; type="application/json"

--this-is-a-boundary
Content-Type: application/json; charset=UTF-8

{"example1": "ok"}

--this-is-a-boundary
Content-Type: application/json; charset=UTF-8

{"example2": "ok"}

--this-is-a-boundary--

However, I find no ways to set the Content-Type header of each Multipart.Bodypart. And I get this warning:

[WARN] [11/01/2019 15:30:38.829] [gateWaySystem-akka.actor.default-dispatcher-5] [BodyPartRenderer$$anon$2$$anon$1(akka://gateWaySystem)] Explicitly set HTTP header 'Content-Type: application/json; charset=UTF-8' is ignored, explicit `Content-Type` header is not allowed. Set `HttpRequest.entity.contentType` instead.

Here is my code:

val body1: JsValue = 
    s"""
       |{
       |  "example1":"ok"
       |}
       """.stripMargin.parseJson

val body2: JsValue = 
    s"""
       |{
       |  "example2":"ok"
       |}
       """.stripMargin.parseJson

val ct = ContentType(MediaType.custom("application/json", false), () => HttpCharsets.`UTF-8`)
val headers = List(`Content-Type`(ct))
val bodyPart1 = Multipart.General.BodyPart.Strict(body1.toJson.compactPrint, headers) 
val bodyPart2 = Multipart.General.BodyPart.Strict(body2.toJson.compactPrint, headers) 

println(bodyPart1.headers)   // print result : List(Content-Type: application/json; charset=UTF-8)

val contentType = MediaTypes.`multipart/related`
        .withParams(Map("type"->"application/json"))

val responseEntity = Multipart.General(contentType, bodyPart1, bodyPart2).toEntity()

HttpResponse(entity = responseEntity)

And the client always gets this reply:

content-type: multipart/related; type="application/json"; boundary=OFi4Sr+6IkXi0JXIEVioSgys

--OFi4Sr+6IkXi0JXIEVioSgys
Content-Type: text/plain; charset=UTF-8

{"example1": "ok"}

--OFi4Sr+6IkXi0JXIEVioSgys
Content-Type: text/plain; charset=UTF-8

{"example2": "ok"}

--OFi4Sr+6IkXi0JXIEVioSgys--

It seems like the source code of Akka Http leaves no place to override this behavior. In akka/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala:

trait PredefinedToEntityMarshallers extends MultipartMarshallers {
  ...
  implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`)
  def stringMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[String] =
    Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(mediaType withCharset cs, s) }
  def stringMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[String] =
    Marshaller.withFixedContentType(mediaType) { s ⇒ HttpEntity(mediaType, s) }
  ...
}

Any suggestions or solutions will be appreciated! Thanks a lot!

jrudolph commented 5 years ago

In

val bodyPart1 = Multipart.General.BodyPart.Strict(body1.toJson.compactPrint, headers) 

the body1.toJson.compactPrint part is implicitly converted into an HttpEntity. You can instead provide an HttpEntity explicitly where you would put in the content type. Something like this:

val bodyPart1 = Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`application/json`, body1.toJson.compactPrint), headers)