I couldn't find any documentation on how to dispatch based on HTTP method (on the same uri). The closest I got was :default-request-type on the define-easy-handler -- but it seems to dispatch to the latter, even though I use GET method:
(define-easy-handler (index :uri "/" :default-request-type :get) ()
(log-message* :info "GET on index ------ ")
(format nil "Hello World"))
(define-easy-handler (echo :uri "/" :default-request-type :post) ()
(log-message* :info "POST on index ------ ")
(format nil "~S" (raw-post-data :force-text t)))
The (perhaps slightly deceptively named) :uri parameter is allowed to be either a string or a predicate on the request object. So, you can pass a function there that checks if the method and path match. I wrote a macro to make it prettier:
(defmacro method-path (methods path)
"Expands to a predicate the returns true of the Hunchtoot request
has a SCRIPT-NAME matching the PATH and METHOD in the list of METHODS.
You may pass a single method as a designator for the list containing
only that method."
(declare
(type (or keyword list) methods)
(type string path))
`(lambda (request)
(and (member (hunchentoot:request-method* request)
,(if (keywordp methods)
`'(,methods)
`',methods))
(string= (hunchentoot:script-name* request)
,path))))
(hunchentoot:define-easy-handler (get-handler :uri (method-path :get "/hello")) ()
"hello!")
(hunchentoot:define-easy-handler (post-handler :uri (method-path (:post :put) "/hello")) ()
"a post or a put!")
In the case where the path is found but the method isn't, we should probably return an HTTP 405 error instead of the 404 error that Hunchentoot returns when no handlers match. In order to do this, you could manually write a catch-all handler for every path you define. The 405 response is supposed to include a list of acceptable methods, and I can't think of an easy way to generate one short of modifying define-easy-handler to support specialization on method directly, which might be a good idea.
Many frameworks built on top of hunchentoot have that. Restas and Caveman are just two examples. For example in Restas you can say:
(restas:define-route foo ("/foo" :method :get)
; some code here
)
(restas:define-route foo/post ("/foo" :method :post)
; some other code here
)
We now have an Hunchentoot add-on to do just that: easy-routes. It brings dispatch by HTTP method, arguments extraction from the url path, and a handy decorator notation.
To use it, we just have to use its routes-acceptor instead of the default easy-acceptor:
(hunchentoot:start (make-instance 'easy-routes:routes-acceptor))
An example:
(defroute foo ("/foo/:arg1/:arg2" :method :get)
(&get w)
(format nil "<h1>FOO arg1: ~a arg2: ~a ~a</h1>" arg1 arg2 w))
Related
I am trying to turn some Julia code into a very basic local microservice, which accepts a POST request with some options supplied via JSON, runs a program, and returns a 200 response. My microservice code is here:
const ROUTERS = Dict()
function __init__()
"""Initialisation function to populate the global variables."""
ROUTERS["ROUTER"] = HTTP.Router()
HTTP.register!(ROUTERS["ROUTER"], "POST", "/run", run_program)
end
function run_program(req)
"""Takes a run request and passes it to the main program, before sending a 200 reponse to say no errors have occured."""
setup = JSON3.read(req.body, Dict{String, Any})
run_program(setup)
return HTTP.Response(200, JSON3.write("Success."))
end
function requestHandler(req)
"""Recieves incoming requests and passes them to the router. Returns the response object."""
local resp
resp = HTTP.handle(ROUTERS["ROUTER"], req)
return resp
end
function run_service(port=8080)
"""Runs the microservice at the specified port."""
HTTP.serve(requestHandler, "0.0.0.0", port)
end
__init__()
This code works with HTTP version 0.9.17, but I updated it to the new version 1.5.5. Now I receive this error whenever I make a request to the running service:
LogLevel(1999): handle_connection handler error
│ exception =
│ UndefVarError: handle not defined
What am I missing here? Have I defined my handler function incorrectly somehow?
There's no HTTP.handle function anymore in HTTP.jl version 1.x, as outlined by the documentation.
You'll probably want something like
"""
Receives incoming requests and passes them to the router. Returns the response object.
"""
function requestHandler(req)
return ROUTERS["ROUTER"](req)
end
instead. Also note that docstrings need to be inserted before the function for the docsystem to pick them up.
I'm setting up a super simple http call to an endpoint on my server which returns a JSON response - an object with a success prop which is a boolean. Here is the relevant code:
getData : Model -> Cmd Msg
getData { userId, data } =
let
url =
"/get-data?userId=" ++ userId ++ "&data=" ++ data
request =
Http.get url decodeGetData
in
Http.send GetDataResult request
decodeGetData : Decode.Decoder Bool
decodeGetData =
Decode.at [ "success" ] Decode.bool
I'm getting the following error from the compiler:
Http.send GetDataResult request
^^^^^^^
Function `send` is expecting the 2nd argument to be:
Http.Request String
But it is:
Http.Request Bool
What's going wrong here? How do I set up Http.send to expect a Bool instead of a string? I know that the basic setup of my request is correct because my code compiles if I change the decodeGetData function to:
decodeGetData : Decode.Decoder String
decodeGetData =
Decode.at [ "success" ] Decode.string
In this case I can successfully make the http request, but then I get an error because the success prop on the response is a boolean instead of a string.
Any pointers? Cheers!
The code you pasted in all looks good, which leads me to think that the problem lies in a piece of code you don't have listed. Namely, the Msg constructor GetDataResult should have a single parameter of type Result Http.Error Bool. The compiler error you received would occur if the signature were instead Result Http.Error String.
It's common practice in some cases to pass plain URIs as suffix of the path instead of a query parameter. Here is an example from Internet Archive's Wayback Machine.
https://web.archive.org/web/20150825082012/http://example.com/
In this example, user is requesting a copy of http://example.com/ as captured at 2015-08-25 08:20:12. If we were to implement similar service in Go, we probably would have a router as follows:
http.HandleFunc("/web/", returnArchivedCopy)
Then in the returnArchivedCopy handler function, we will split r.URL.Path (where r is the Request object) to extract the date-time and the target URL. However there is a problem in this style of URL scheme; Go's net/http package calls cleanPath function on the path portion to sanitize it. This sanitization process does various cleanup tasks such as eeliminating . and .. from the path and replace multiple slashes with a single one. This later operation makes sense when because in Unix systems // in the file path are same as /. However this causes an issue in the above described use case as http://example becomes http:/example and the server internally returns a redirect response to the client with the sanitized path.
I am wondering, what are my options in this case? Is there a way to ask HTTP not to sanitize the request path while still utilizing all the default behavior that is shipped with the default (or slightly modified) server, multiplexer, and handler? Or is there a way to modify the request parameters (path in this case) before it hits the multiplexer's routing patterns. If the later is possible, we might try to perform something like URL encoding to avoid the redirect and later decode the URL back in the handler function before extracting desired bits.
I have experimented with some custom handlers and multiplexers, but I am new to Go, hence I was not quite sure how to delegate the routing back to the default handlers after making changes in the request.
You can implement a wrapper mux, that falls back to the default one, here's a very simple example:
func main() {
http.HandleFunc("/blah", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("w00t"))
})
http.ListenAndServe(":9090", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
p := strings.SplitN(req.URL.RequestURI()[1:] /*trim the first slash*/, "/", 3)
if len(p) != 3 || p[0] != "web" {
http.DefaultServeMux.ServeHTTP(w, req)
return
}
t, err := time.Parse("20060102150405", p[1])
if err != nil {
http.Error(w, "invalid time", 400)
return
}
url := p[2]
fmt.Fprintf(w, "requested url %v # %v", url, t)
}))
}
The result of a PUT operation against a database, is sometimes ok even if it doesn't come back as a HTTP code 200.
In this specific case I want to treat 400 as another okay error, but struggle to come up with an elegant way of doing it.
# stream from a promise any non 200 is fail
putter = Bacon.fromPromise #_exec 'PUT', '/some/resource', {}
errors = putter.errors().mapError(errBody) # now errors are normal values
badErrors = errors.filter(statusIsnt(400)).flatMap (body) ->
return new Bacon.Error body # and now they are errors again
okErrors = errors.filter(statusIs(400)).flatMap (body) -> {}
noError = putter.mapError().filter (v) -> v? # attempt to get rid of errors
Bacon.mergeAll noError, okErrors, badErrors # combine to get result stream
I come from a promises background, and I find the above somewhat clumsy, which leads me to conclude I'm missing something. Compare:
#_exec 'PUT', '/some/resource', {}
.fail (err) ->
if err.body.status == 400 # ok
return {}
else
throw err
You can use withHandler with Bacon.Error for throw and (implicit) Bacon.Next for result. But the code will look very similar to your handling already in promise. Actually promise version has less boilerplate.
Compare your promise version
promise = #_exec('PUT', '/some/resource', {})
recovered = promise.fail (err) ->
# 400 is ok. make it ok already in promise
if err?.body?.status == 400 then {} else throw err.body
result = Bacon.fromPromise recovered
and withHandler Bacon "only" version
promise = #_exec('PUT', '/some/resource', {})
stream = Bacon.fromPromise promise
result = stream.withHandler (event) ->
if (event.hasValue())
#push(event)
else
# error
if event.error?.body?.status == 400 then
#push({})
else
#push(event)
One possible solution for this specific scenario is to amend the result already in the promise, but I can easily envisage cases where you can't do that.
Bacon.fromPromise #_exec('PUT', '/some/resource', {}).fail (err) ->
# 400 is ok. make it ok already in promise
if err?.body?.status == 400 then {} else throw err.body
Surely there must be a better functional approach to this problem?
withHandler is an underlying method that lets you operate on Events of all types. Within it you can call #push, which will send errors to the error stream and values to the value streeam. Perhaps the following:
Bacon.fromPromise(#_exec('PUT', '/some/resource', {}))
.withHandler((event) ->
if event.isError && event.error.body?.status? == 400
#push(new Bacon.Next({}))
else
#push(event)
)
I am getting my feet wet with Meteor's HTTP methods.
As a test, I am hitting Twitter's api with a method on the server, as follows:
"twitter_user": () ->
Meteor.http.get("https://api.twitter.com/1/users/show.json", {screen_name:"ppedrazzi"})
On the client, I call the method as follows:
twitterUser = Meteor.call("twitter_user")
When trying to access the twitterUser object, it shows as undefined. I was expecting to be able to use twitterUser.data.id or twitterUser.data.name to grab the fields from the resulting JSON, but no luck (since the object does not exist).
Incidentally, if I drop the URL into the browser, I get a JSON object on the page, see example here:
https://api.twitter.com/1/users/show.json?screen_name=ppedrazzi
You should use an async call to your method:
Meteor.call "twitter_user", (error, result) ->
twitterUser = result
Citation from the documentation:
On the client, if you do not pass a callback and you are not inside a stub, call will return undefined, and you will have no way to get the return value of the method. That is because the client doesn't have fibers, so there is not actually any way it can block on the remote execution of a method.
Note that in this particular case, you can run Meteor.http.get directly on the client:
Meteor.http.get(
"https://api.twitter.com/1/users/show.json",
screen_name:"ppedrazzi",
(error, result) -> twitterUser = result
)