Anyone have a worked example of properly setting up an endpoint that accepts a multipart-data body? I’m consistently getting Invalid value for: body (expected digit got '--0db4...' (line 1, column 1)) My current application is stuck on 0.19, but I’ve spiked a 1.x upgrade with the same result. I’ve also tried with both the Akka-HTTP backend and the HTTP4S backend with the same result.
import io.circe.Json
import sttp.model.{Part, StatusCode}
import sttp.tapir._
import sttp.tapir.json.circe._
import java.io.File
import sttp.tapir.generic.auto._
import io.circe.generic.auto._
case class MultipartData(operations: Json, file: Part[File])
object Endpoints {
val uploadEndpoint
: PublicEndpoint[MultipartData, Unit, Json, Any] =
endpoint.post
.in("upload")
.in(multipartBody[MultipartData])
.out(jsonBody[Json])
}
The requests I’m sending to this endpoint look like this
POST /upload HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Host: localhost:8080
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Content-Disposition: form-data; name="file"; filename="Test_Upload.csv"
Content-Type: text/csv
... CSV Data ...
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Content-Disposition: form-data; name="operations"
... Some text ...
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1--
And the response is
HTTP/1.1 400 Bad Request
Content-Length: 75
Content-Type: text/plain; charset=UTF-8
Date: Mon, 06 Feb 2023 18:24:01 GMT
Server: akka-http/10.2.6
Invalid value for: body (expected digit got '--13d6...' (line 1, column 1))
Unfortunately I can’t reproduce this. My test server is as follows:
package sttp.tapir.examples
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import sttp.model.Part
import sttp.tapir._
import sttp.tapir.generic.auto._
import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
import java.io.File
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
case class MultipartData(operations: String, file: Part[File])
object X extends App {
val uploadEndpoint: PublicEndpoint[MultipartData, Unit, Unit, Any] =
endpoint.post
.in("upload")
.in(multipartBody[MultipartData])
//
implicit val actorSystem: ActorSystem = ActorSystem()
import actorSystem.dispatcher
// starting the server
val routes = AkkaHttpServerInterpreter().toRoute(List(uploadEndpoint.serverLogicPure[Future] { r =>
println(r)
Right(())
}))
val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(routes).map { _ =>
println("Press any key to exit ...")
scala.io.StdIn.readLine()
}
// cleanup
Await.result(bindAndCheck.transformWith { r => actorSystem.terminate().transform(_ => r) }, Duration.Inf)
}
I’m sending the request as you gave it, from Idea’s HTTP client:
POST /upload HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Host: localhost:8080
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Content-Disposition: form-data; name="file"; filename="Test_Upload.csv"
Content-Type: text/csv
... CSV Data ...
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1
Content-Disposition: form-data; name="operations"
... Some text ...
--13d6528ff0084c82-7962ea0aa0fb1db2-44e7437e0695a499-ad43feb4ebb453c1--
And I’m getting a 200 OK, on the backend the println is as follows:
MultipartData(... Some text ...,Part(file,/var/folders/99/1jlks6pn7gz_7mdlt4n3hkbm0000gn/T/tapir6671885412493013947tmp,TreeMap(filename -> Test_Upload.csv),List(Content-Transfer-Encoding: binary, Content-Type: text/csv)))
So there must be sth different in your setup which causes the issue to surface
Thank you so much for taking the time to (attempt to) reproduce. It helped me locate the source of my problem. I thought the issue was in the endpoint definition itself, but it was actually in the route definition where I was stitching all my endpoints together.
The real issue was that my real application has a second endpoint.post with no path qualification, and in my toRoutes call I had that route listed before the POST /upload, which I take to mean when the route matcher is constructed my upload calls were being fed to the unqualified endpoint and never reaching the upload route, at all. Reordering the endpoint list resolved the issue.
I’ll be much more careful about that and use .endpoint("") in the future. Thanks again.
Oddly enough, I have a test for shadowed endpoints for exactly that reason, but it didn’t detect this particular overlap. I’ll see if I can figure out why that is.