I have run into a problem with a Play 2.1.0 form that contains a file upload and an additional input field. I use
def uploadTaxonomy() = Action(parse.multipartFormData) {
implicit request =>
request.body.file("xml").map { file =>
val xml = scala.io.Source.fromFile(file.ref.file).mkString
taxonomyForm.bindFromRequest().fold(
formWithErrors => BadRequest(views.html.index(formWithErrors)),
result => {
Taxonomies.create(result._1, xml)
Redirect(routes.Application.index())
}
)
}.getOrElse {
Redirect(routes.Application.index())
}
}
and my form is this:
val taxonomyForm = Form(
tuple(
"label" -> text,
"xml" -> text
)
)
The problem is that bindFromRequest() always fails (causing a bad request to be returned to the client).
Does anybody have an idea where the problem might lie?
Note: I am aware that there is a bug in 2.1.0 that manifests when no files are selected in an upload field; it does not seem to be related, however.
As far as I know the xml should not be part of the form definition as you get it directly from the request body.
Related
I have the following code for the download of a file and then redirecting to a thank you page in the DownloadController.scala under the Play Framework
def thankView = SecuredAction(WithProvider[AuthType](CredentialsProvider.ID)) {
implicit request: SecuredRequest[DefaultEnv, AnyContent] =>
Ok(downloadThankView(request.identity))
}
def download = SecuredAction(WithProvider[AuthType](CredentialsProvider.ID)) {
implicit request: SecuredRequest[DefaultEnv, AnyContent] =>
val futureMaybeFile = downloadService.generateDownload(request.identity.userID)
val maybeFile = Await.result(futureMaybeTempFile, 10 second)
maybeFile match {
case Some(file) =>
Ok.sendFile(
file,
fileName = _ => Some(file.getName),
onClose = () => {
file.delete()
// position (1) for placing the redirect, which doesn’t prevent the file download, but does not get executed
Redirect(routes.DownloadController.thankView())
}
).withHeaders(
"HttpResponse.entity.contentType" -> "text/txt",
"Content-Disposition" -> s"attachment; filename=${file.getName}"
)
// position (2) for placing the redirect which executes but prevents the file download
Redirect(routes.DownloadController.thankView())
case None =>
Redirect(routes.InfoController.view(Messages(“oops.there.was.error"), Messages("download.title")))
}
}
When I place the Redirect(routes.DownloadController.thankView()) line in the onClose section of sendFile, it does not get executed; and when it is placed after the sendFile it executes but prevents the file from getting downloaded.
What am I missing in here? How can I solve this issue?
From a HTTP point of view, I think this is not something doable: a single HTTP response cannot contain at the same time a file and be a redirect to somewhere else.
You should rather have some kind of html page that:
triggers the file download (via JS for instance)
triggers a redirect to the "thank you" page
I'm using play to proxy my API calls from the ui. For example POST:
def post(url: String) = Action { implicit request =>
Async {
WS.url(proxyUrl + request.uri)
.withQueryString(request.queryString.mapValues(_.head).toSeq: _*)
.withHeaders(request.headers.toMap.mapValues(_.head).toSeq: _*)
.post(request.body.asJson).map(response => Ok(response.body))
}
}
but this can only handle "application/json" and "text/json" content types. But now I want to make requests with custom content type: "application/vnd.MyCustomType-v1+json;charset=utf-8" and of course it doesn't work with current implementation. Have tried different solutions, but nothing seems to work. Any ideas?
I'm using play 2.1
The source for the json body parser looks like this:
def json(maxLength: Int): BodyParser[JsValue] = when(
_.contentType.exists(m => m.equalsIgnoreCase("text/json") || m.equalsIgnoreCase("application/json")),
tolerantJson(maxLength),
createBadResult("Expecting text/json or application/json body")
)
tolerantJson is, itself, a body parser that does the json parsing without a check of the content-type header, so you should just be able to use that to parse your request instead of parse.json.
If you want to go further and have a parser that checks your specific content-type header then you could use
when(
_.contentType.exists(m => m.equalsIgnoreCase(expectedContentType)),
tolerantJson(maxLength),
createBadResult("Wrong content type")
)
to create your own parser.
I have the following Controller action as form post resolver:
def importCompletionsSubmit(indexName: String) = AuthenticatedAction {
Action.async {
implicit request => {
completionsForm.bindFromRequest().fold(
errors => Future.successful(Ok(html.crudindex.importCompletionsForm(indexName, errors))),
completions => {
val autoCompletionService = new AutoCompletionService(new Elasticsearch)
autoCompletionService.importCompletions(indexName, completions.text) map {
result: BulkImportResult =>
if (result.error) Redirect(routes.ListIndices.index(Option.empty[String])).flashing("error" -> Messages("error.bulkItemsFailed", result.failures))
else Redirect(routes.ListIndices.index(Option.empty[String])).flashing("success" -> Messages("success.completionsAdded", result.requests))
}
}
)
}
}
}
I know that I can change the max length value for this action but what I would like to do is sending the user back to the form with a nice error message when he enters too much text.
If the request body is exceeding the default max length I get a completly blank page and only the browser console shows "413 (Request Entity Too Large)". I tried to catch this error in my global object but that did not change anything. It seems to me that the global onError trigger is not entered when a parser sends back an errorpage. Still a blank page. I also tried to catch that error inside the action but it seems to me that the action code is not entered because the body parser is already throwing this blank error page.
Is there a way to send the user back to the form action when the body exceeds the max length?
Something like this should work for you:
def test = Action.async(parse.maxLength(1024, parse.multipartFormData)) { implicit request =>
Future(request.body match {
case Left(MaxSizeExceeded(length)) => Ok(your_pretty_error_page.scala.html)
case Right(body) => {
...
}
})
}
According to this guide, one can upload files by writing the html form by hand. I want to handle file upload as part of a bigger form that includes text fields (for example name and email). Here is what I have to far (quite ugly):
def newUser = Action(parse.multipartFormData) { implicit request =>{
//handle file
import play.api.mvc.MultipartFormData.FilePart
import play.api.libs.Files.TemporaryFile
var uploadSuccessful = true
var localPicture: FilePart[TemporaryFile] = null
request.body.file("picture").map { picture =>
localPicture = picture }.getOrElse {
uploadSuccessful = false }
//process the rest of the form
signupForm.bindFromRequest.fold(
errors => BadRequest(views.html.signup(errors)),
label => {
//file uploading code here(see guide), including error checking for the file.
if(uploadSuccesful){
User.create(label._1, label._2, label._3._1, 0, "NO PHOTO", label._4)
Redirect(routes.Application.homepage).withSession("email" -> label._2)
} else {
Redirect(routes.Application.index).flashing(
"error" -> "Missing file"
}
})
} }
This looks tremendously ugly to me. Note that I have defined a signupForm somewhere that includes all fields (apart from the file upload one). My question is: Is there a prettier way of going about this? Perhaps by including the file field in the signupForm and then handling errors uniformly.
So far I think it's not possible to bind binary data to a form directly, you can only bind the reference (e.g. the picture's ID or name). You could however reformulate your code a bit:
def newUser() = Action(parse.multipartFormData) { implicit request =>
import play.api.mvc.MultipartFormData.FilePart
import play.api.libs.Files.TemporaryFile
request.body.file("picture").map { picture =>
signupForm.bindFromRequest.fold(
errors => BadRequest(views.html.signup(errors)),
label => {
User.create(label._1, label._2, label._3._1, 0, picture.absolutePath(), label._4)
Redirect(routes.Application.homepage).withSession("email" -> label._2)
}
)
}.getOrElse(Redirect(routes.Application.index).flashing("error" -> "Missing file"))
}
You can use the asFormUlrEncoded, like below:
def upload = Action(parse.multipartFormData) { request =>
val formField1 = request.body.asFormUrlEncoded("formField1").head;
val someOtherField = request.body.asFormUrlEncoded("someOtherField").head;
request.body.file("photo").map { picture =>
...
}
}
In Rails, I was able to do something similar to the following:
respond_to do |format|
format.xml { ... }
format.json { ... }
end
and the appropriate block would be executed based on what the client supplied in the Accept header.
How can I do the same thing in Play 2.0 (Scala)?
I'd look to do something that looks roughly like this:
try {
Resources.delete(id)
Ok("done")
}
catch {
case e: ClientReportableException =>
?? match {
case "application/xml" => Ok(<error>{e.message}</error>)
case "application/json" => Ok(...)
}
}
Is there a Play idiom for this, or do I just fetch the value of the Accept header from the request?
In Play 2.1 you can write the following:
request match {
case Accepts.Xml() => Ok(<error>{e.message}</error>)
case Accepts.Json() => Ok(…)
}
The cases statements are tried in the order they are written, so if your client sets the HTTP Accept header to */* the first one will match (in this example case Accepts.Xml()). So, you usually want to write the Accepts.Html() case first because browsers set the Accept header to */*.
(Note: you may also be interested in this answer for a similar question in Java)
I have just released a Play! 2.0 module for content negotiation called mimerender (http://github.com/martinblech/play-mimerender).
The idea is that you have to define a mapping from your domain classes to different representations:
val m = mapping(
"text/html" -> { s: String => views.html.index(s) },
"application/xml" -> { s: String => <message>{s}</message> },
"application/json" -> { s: String => toJson(Map("message" -> toJson(s))) },
"text/plain" -> identity[String]_
)
Once you have done that once, you can reuse that mapping throughout all your controllers:
object Application extends Controller {
def index = Action { implicit request =>
m.status(200)("Hello, world!")
}
}
Please note it's a very early release and has only been tested on Play 2.0.4