Downloading file from POST request with Elm - post

I'm in the position that I have an HTTP POST endpoint /render that returns a PDF document, and would like to present a button/link to the user that will cause this document to be downloaded and saved to a file without navigating away from my Elm app.
Ideally the POST will accept a text/plain body with a custom format, but I could rework the endpoint to accept multipart/form-data or application/x-www-form-urlencoded.
I can download the raw data to the Elm app successfully as follows, but I'm at a loss for how to save the file to disk.
import Http
render : String -> Http.Request String
render body =
Http.request
{ method = "POST"
, headers = []
, url = "/render"
, body = Http.stringBody "text/plain" body
, expect = expectString
, timeout = Nothing
, withCredentials = False
}

I did it using expectBytes rather then expectString
so my code is
import Bytes exposing (Bytes)
import File.Download as Download
Msg = .. | FormUploaded (Result Http.Error Bytes)
Http.post
{ url = "/passports"
, body =
Http.multipartBody...
, expect = Http.expectBytesResponse FormUploaded (resolve Ok)
}
downloadPdf : Bytes -> Cmd msg
downloadPdf pdfContent =
Download.bytes "form.pdf" "application/pdf" pdfContent
update : Msg -> Model -> ( Model, Cmd Msg )
update model =
...
FormUploaded (Ok response) ->
( model, downloadPdf response )
FormUploaded (Err err) ->
( model, Cmd.none )
-- this helper function copied from https://github.com/elm/http/blob/2.0.0/src/Http.elm#L514-L521
resolve : (body -> Result String a) -> Http.Response body -> Result Http.Error a
resolve toResult response =
case response of
BadUrl_ url ->
Err (BadUrl url)
Timeout_ ->
Err Timeout
NetworkError_ ->
Err NetworkError
BadStatus_ metadata _ ->
Err (BadStatus metadata.statusCode)
GoodStatus_ _ body ->
Result.mapError BadBody (toResult body)
it's not ideal but works
PS: I got help from Elm Slack Channel https://elmlang.slack.com/

Related

How do I chain a Task and Nav.pushUrl in Elm?

In an Elm app I have a Msg handler LinkedClicked that returns a Cmd Msg:
I want to perform two action for LinkedClicked: 1. to update the DOM by updating my Model and 2. reset the page to display from the top by calling Dom.setViewport
Is it possible (and desirable) to do away with ChangeUrl Msg and instead do Nav.pushUrl in my LinkedClicked handler. My approach by firing off a Msg within a Msg feels brittle.
jump_to_url : Url.Url -> Cmd Msg
jump_to_url url =
Task.perform ( \_ -> ChangeUrl url) (Dom.setViewport 0 0)
on_birthdate_url : Model -> Url.Url -> ( Model, Cmd Msg )
on_birthdate_url model url =
( { model
| ...
}
, jump_to_url url
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkedClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
on_birthdate_url model url
...
ChangeUrl url ->
( model
, Nav.pushUrl model.key (Url.toString url)
)
After reading everyone's comments I came to these conclusions:
I don't want the refreshing of the page url and the resetting of the viewport to happen concurrently. The former needs to happen before the latter otherwise the viewport reset can happen too early.
renaming the function jump_to_url to new_top_page encapsulates both the url update and viewport update.
I'm still wondering if encapsulating two actions into one function is the elm way.
new_top_page : Url.Url -> Cmd Msg
new_top_page url =
Task.perform ( \_ -> ChangeUrl url) (Dom.setViewport 0 0)
on_birthdate_url : Model -> Url.Url -> ( Model, Cmd Msg )
on_birthdate_url model url =
( { model
| ...
}
, new_top_page url
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkedClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
on_birthdate_url model url
...
ChangeUrl url ->
( model
, Nav.pushUrl model.key (Url.toString url)
)

F# making a API request to a graphql endpoint

I try to get some data from a grqphql endpoint with F#.
I use Fsharp.Data
let apiQuery = """query findData {
apiData(Model:{
PageNumber: 1,
PageSize: 100
})
{
ErrorMessage Success ValidationResult TotalCount
Data{
ItemId
}
}
}"""
let queryGraphQl () =
Http.RequestString
( apiUrl,
headers = [ ContentType HttpContentTypes.Json;
Authorization ("bearer " + token)
],
body =
TextRequest apiQuery
)
But I get (500) Internal Server Error
The same in Python works fine:
query_headers = {
"Authorization": 'bearer %s' % token,
'Content-Type': 'application/json'
}
response = requests.post(url, json={'query': apiQuery}, headers=query_headers)
Any suggestions what I am missing?
In Postman I have to add
Content-Length and Host like to be calculated when request is sent.
It appears that the F# and Python code is not equivalent. The Python code contains additional query keyword in the payload.
I don't know the specifics of your particular endpoint, but I wrote similar code using one of the public interfaces.
open System.Net
open FSharp.Data
open FSharp.Data.HttpRequestHeaders
let key = "********-****-****-****-*************"
let uri k = $"https://api.everbase.co/graphql?apikey={k}"
let gurl = uri key
let apiQuery = """{ "query" :
"{ client { ipAddress { country { name } city { name } } } }"
}"""
let connectToGraph apiUrl apiQuery =
try
let res = Http.RequestString( url = apiUrl, httpMethod="POST", body = TextRequest apiQuery, headers = [ ContentType HttpContentTypes.Json; UserAgent "mozilla" ])
res
with
| _ as ex -> ex.Message
[<EntryPoint>]
let main argv =
let res = connectToGraph gurl apiQuery
printf "Response: %A" res
0
I suppose you should separate the query in your F# code from the rest of the definition with a ':'. Also the actual payload should be wrapped in quotes/double quotes to form a valid Json value.

Uploading txt file via POST request with HttpBuilder

I want to upload a txt file to a website using a POST request with HTTPBuilder and multipart/form-data
I've tried running my function and I get a HTTP 200 OK response, but the file doesn't appear on the website anywhere.
private Map fileUpload(String url, File file){
log.debug "doPost: $url body: ${file.getName()}"
FileBody fileBody = new FileBody(file,ContentType.APPLICATION_OCTET_STREAM)
def result = [:]
try {
def authSite = new HTTPBuilder(url)
authSite.auth.basic(user, password)
authSite.request(POST) { req ->
headers.Accept = "application/json, text/javascript, */*; q=0.01"
req.params.setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000)
req.params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 60000)
def mpe = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
mpe.addPart("gxt",fileBody)
req.setEntity(mpe)
response.success = { resp, reader ->
result = reader
}
response.failure = { resp, reader ->
println "My response handler got response: ${resp.statusLine}"
}
}
}
catch (e) {
log.debug("Could not perform POST request on URL $url", e)
throw e
}
return result
}
From debugging this is the status recieved
3695 [main] DEBUG org.apache.http.wire - << "HTTP/1.1 200 OK[\r][\n]"
3695 [main] DEBUG org.apache.http.wire - << "Date: Thu, 10 Jan 2019 07:34:06 GMT[\r][\n]"
Anything I'm doing wrong? I don't get any errors but it just seems like nothing happens.
I don't have anything conclusive, but I suspect there is something invalid with the way you set up the multipart upload.
To help figure this out, below is a standalone, working, multipart upload groovy script using HttpBuilder:
#Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.7.1')
#Grab('org.apache.httpcomponents:httpmime:4.2.1')
import org.apache.http.entity.mime.content.*
import org.apache.http.entity.mime.*
import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.Method.POST
fileUpload('https://httpbin.org/post', new File('data.txt'))
Map fileUpload(String url, File file){
println "doPost: $url body: ${file.name}"
def result
try {
new HTTPBuilder(url).request(POST) { req ->
requestContentType = "multipart/form-data"
def content = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
content.addPart(file.name, new InputStreamBody(file.newInputStream(), file.name))
req.entity = content
// json might be something else (like a reader)
// depending on the response content type
response.success = { resp, json ->
result = json
println "RESP: ${resp.statusLine}, RESULT: $json"
}
response.failure = { resp, json ->
println "My response handler got response: ${resp.statusLine}"
}
}
} catch (e) {
println "Could not perform POST request on URL $url"
throw e
}
result
}
The script assumes a file data.txt with the data to post in the current directory. The script posts to httpbin.org as a working test endpoint, adjust accordingly to post to your endpoint instead.
Saving the above in test.groovy and executing will yield something like:
~> groovy test.groovy
doPost: https://httpbin.org/post body: data.txt
RESP: HTTP/1.1 200 OK, RESULT: [args:[:], data:, files:[data.txt:{ "foo": "bar" }], form:[:], headers:[Accept:*/*, Connection:close, Content-Type:multipart/form-data; boundary=ZVZuV5HAdPOt2Sv7ZjxuUHjd8sDAzCz9VkTqpJYP, Host:httpbin.org, Transfer-Encoding:chunked], json:null, origin:80.252.172.140, url:https://httpbin.org/post]
(note that first run will take a while as groovy grapes need to download the http-builder dependency tree)
perhaps starting with this working example and working your way back to your code would help you pinpoint whatever is not working in your code.

Groovy httpbuilder post list params

I'm trying to consume a web service from my grails project. I'm using httpbuilder 0.7.2. Below is my http client.
static def webServiceRequest(String baseUrl, String path, def data,method=Method.GET,contentType=ContentType.JSON){
def ret = null
def http = new HTTPBuilder(baseUrl)
http.request(method, contentType) {
uri.path = path
requestContentType = ContentType.URLENC
if(method==Method.GET)
uri.query = data
else
body = data
headers.'User-Agent' = 'Mozilla/5.0 Ubuntu/8.10 Firefox/3.0.4'
response.success = { resp, json ->
println "response status: ${resp.statusLine}"
ret = json
println '--------------------'
}
}
return ret
}
The issue is coming when i'm trying to send something like this:
def input = [:]
input['indexArray'] = [1,5]
api call
def response = webServiceRequest(url,uri,input,Method.POST)
when i'm printing the value of post data in my server it shows only last value of list.
{"indexArray":"5"}
it should show both 1 and 5
If you want to send json data using contenttype application/x-www-form-urlencoded you have to explicitly convert the data before adding it to the body, you can use (data as JSON).
I am using RESTClient (nice convenience wrapper on HTTPBuilder, https://github.com/jgritman/httpbuilder/wiki/RESTClient). It is as simple as this with Spock.
RESTClient restClient = new RESTClient("http://localhost:8080")
restClient.contentType = ContentType.JSON
Also it automatically parses the JSON data, so my Spock test is:
when: "we check the server health"
HttpResponseDecorator response = restClient.get([path : "/health"]) as HttpResponseDecorator
then: "it should be up"
response != null
200 == response.status
'application/json' == response.contentType

Using node modules from fable

I'm trying to import a node module in my fable code. Being new to fable I did expect so problems and understanding the import flow seems to be one of those. I have the below code which compiles fine but fails run time with Cannot read property 'request' of undefined on the line of the printfn statement
module Session =
let inline f (f: 'a->'b->'c->'d) = Func<_,_,_,_> f
[<Import("default","request")>]
type Http =
abstract request : string -> System.Func<obj,obj,obj,unit> -> unit
let http : Http = failwith "js only"
let start () =
http.request "http://dr.dk" (ff (fun error response body ->
printfn "%A" body
))
do
start()
I was able to get your example working in the Fable REPL:
open System
open Fable
open Fable.Core
open Fable.Core.JS
open Fable.Core.JsInterop
type RequestCallback = Func<obj, obj, obj, unit>
type Request = Func<string, RequestCallback, unit>
[<ImportDefault("request")>]
let request : Request = jsNative
let start () =
request.Invoke("http://dr.dk", (fun error response body ->
console.log("error", error)
console.log("response", response)
console.log("body", body)
))
start ()
And here is the JavaScript that it generated:
import request from "request";
import { some } from "fable-library/Option.js";
export function start() {
request("http://dr.dk", (error, response, body) => {
console.log(some("error"), error);
console.log(some("response"), response);
console.log(some("body"), body);
});
}
start();
Note that there are bindings for many modules already. For this particular task, I would suggest using Fable.Fetch. If you want a library that works in the browser and .NET, try Fable.SimpleHttp.

Resources