How to set HTTP status code manually in IHP response - haskell

Context
I am using EasyCron (https://www.easycron.com) to ping an endpoint in my IHP app.
If the code on my app's side fails for some reason, I would like to set the HTTP status code to 503 or similar and display the errors in the response so that,
the cron job is marked as failed on EasyCron and
I can see the relevant IHP errors in EasyCron's cron job records.
Since my response is not going to be parsed by EasyCron, I don't need it to be in JSON format. It's just going to be saved in the logs.
Question
I see that there is renderJsonWithStatusCode but since I have no need to parse the JSON and it is extra work to create JSON instances of my data, I would prefer to render a plain text response but with a custom status code. Is this possible?

You can set a custom status code by calling IHP's respondAndExit with a custom response:
import Network.Wai (responseLBS)
import Network.HTTP.Types (status503)
import Network.HTTP.Types.Header (hContentType)
action MyAction = do
let text = "An error happend"
respondAndExit $ responseLBS status503 [(hContentType, "text/plain")] text

Related

Setup Wai Middleware request logger to drop (not log) some requests

I have an app where the Nginx reverse proxy is making a lot of requests to my health endpoint. I'd like to not log these at all so my output logs are smaller. I'm also logging everything as JSON using Network.Wai.Middleware.RequestLogger.JSON, which has a function to format log messages as JSON.
One thing I could do is log an empty bytestring, but I was thinking there may be some way to no-op the log call. I can't figure out how to do that from looking at the various RequestLogger functions in wai-extra.
Does anyone have a recommendation for how to build a custom Middleware in order to not-log certain requests?
I created a custom formatter in the following way:
-- | Wai Application Middleware logger
jsonRequestLogger :: IO Middleware
jsonRequestLogger = mkRequestLogger
$ def { outputFormat = CustomOutputFormatWithDetails dontLogHealthEndpoint }
dontLogHealthEndpoint :: OutputFormatterWithDetails
dontLogHealthEndpoint date req status responseSize duration reqBody response =
if B.isInfixOf "health" $ rawPathInfo req
then toLogStr B.empty
else formatAsJSON date req status responseSize duration reqBody response
This seems to work fine. However, I'd still like to know if there's a better way.

Sending data in GET request Python

I know that it is not an advisable solution to use GET however I am not in control of how this server works and have very little experience with requests.
I'm looking to add a dictionary via a GET request and was told that the server had been set up to accept this but I'm not sure how that works. I have tried using
import requests
r = request.get('www.url.com', data = 'foo:bar')
but this leaves the webpage unaltered, any ideas?
To use request-body with a get request, you must override the post method. e.g.
request_header={
'X-HTTP-Method-Override': 'GET'
}
response = requests.post(request_uri, request_body, headers=request_header)
Use requests like this pass the the data in the data field of the requests
requests.get(url, headers=head, data=json.dumps({"user_id": 436186}))
It seems that you are using the wrong parameters for the get request. The doc for requests.get() is here.
You should use params instead of data as the parameter.
You are missing the http in the url.
The following should work:
import requests
r = request.get('http://www.url.com', params = {'foo': 'bar'})
print(r.content)
The actual request can be inspected via r.request.url, it should be like this:
http://www.url.com?foo=bar
If you're not sure about how the server works, you should send a POST request, like so:
import requests
data = {'name', 'value'}
requests.post('http://www.example.com', data=data)
If you absolutely need to send data with a GET request, make sure that data is in a dictionary and instead pass information with params keyword.
You may find helpful the requests documentation

Return from before_request() in flask

I'm new to flask and currently converting an existing WSGI application to run through flask as long term it'll make life easier.
All requests are POST to specific routes however the current application inspects the post data prior to executing the route to see if the request needs to be run at all or not (i.e. if an identifier supplied in the post data already exists in our database or not).
If it does exist a 200 code and json is returned "early" and no other action is taken; if not the application continues to route as normal.
I think I can replicate the activity at the right point by calling before_request() but I'm not sure if returning a flask Response object from before_request() would terminate the request adequately at that point? Or if there's a better way of doing this?
NB: I must return this as a 200 - other examples I've seen result in a redirect or 4xx error handling (as a close parallel to this activity is authentication) so ultimately I'm doing this at the end of before_request():
if check_request_in_progress(post_data) is True:
response = jsonify({'request_status': 'already_running'})
response.status_code = 200
return response
else:
add_to_requests_in_progress(post_data)
Should this work (return and prevent further routing)?
If not how can I prevent further routing after calling before_request()?
Is there a better way?
Based on what they have said in the documents, it should do what you want it to do.
The function will be called without any arguments. If the function returns a non-None value, it’s handled as if it was the return value from the view and further request handling is stopped.
(source)
#app.route("/<name>")
def index(name):
return f"hello {name}"
#app.before_request
def thing():
if "john" in request.path:
return "before ran"
with the above code, if there is a "john" in the url_path, we will see the before ran in the output, not the actual intended view. you will see hello X for other string.
so yes, using before_request and returning something, anything other than None will stop flask from serving your actual view. you can redirect the user or send them a proper response.

Python3 - Error posting data to a stikked instance

I'm writing a Python 3 (3.5) script that will act as a simple command line interface to a user's stikked install. The API is pretty straight forward, and its documentation is available.
My post function is:
def submit_paste(paste):
global settings
data = {
'title': settings['title'],
'name': settings['author'],
'text': paste,
'lang': settings['language'],
'private': settings['private']
}
data = bytes(urllib.parse.urlencode(data).encode())
print(data)
handler = urllib.request.urlopen(settings['url'], data)
print(handler.read().decode('utf-8'))
When I run the script, I get the printed output of data, and the message returned from the API. The data encoding looks correct to me, and outputs:
b'private=0&text=hello%2C+world%0A&lang=text&title=Untitled&name=jacob'
As you can see, that contains the text= attribute, which is the only one actually required for the API call to successfully work. I've been able to successfully post to the API using curl as shown in that link.
The actual error produced by the API is:
Error: Missing paste text
Is the text attribute somehow being encoded incorrectly?
Turns out the problem wasn't with the post function, but with the URL. My virtual host automatically forwards http traffic to https. Apparently, Apache drops the post variables when it forwards.

Unexpected conduit behaviour with wai

I'm trying to write a really trivial "echo" webapp using wai; all I want it to do is reply with the data that is POSTed to it (I really don't care about the method, but I'm using curl and curl is using POST, so that's what I'm going for). My trivial web server is this:
import Network.Wai
import Network.HTTP.Types (status200)
import Network.Wai.Handler.Warp (run)
import Control.Monad.Trans ( liftIO )
import Blaze.ByteString.Builder.ByteString (fromByteString)
import qualified Data.Conduit.List as CondList
import Data.Conduit ( ($$), ($=), Flush(Chunk) )
application req = do
let src = requestBody req $= CondList.map (Chunk ∘ fromByteString)
return $ ResponseSource status200 [("Content-Type", "text/plain")] src
main = run 3000 application
What I expected this to do is basically tie the request body to the response body, so that when I run curl --data #/usr/share/dict/words localhost:3000; it would spit my words file back at me. Instead, it gives an empty body. Running curl with "-v" shows that my app is replying with a "200 OK" and no data. I'm not sure what I'm doing wrong here.
If I replace the application function with this:
_ ← requestBody req $$ CondList.mapM_ (liftIO ∘ print)
return $ responseLBS status200 [("Content-Type", "text/plain")] "Hello world\n"
and add an OverloadedStrings pragma to allow the "Hello World" part to work, then I do see my app printing the entire request body to stdout, so I know that curl is submitting the data properly. I also get "Hello World" printed to the curl stdout, so I know that curl works as I expect it to. I must be doing something wrong where I'm tying my requestBody to my ResponseSource, but I don't see it.
You're correctly using conduit, the problem is that the streaming behavior you're trying to get cannot reliably work in the context of HTTP. Essentially, you want to start sending the response body while the client is sending the request body. This could lead to a deadlock, as both the client and server could be stuck in send mode. To avoid this, Warp flushes the request body before sending the response, which is why the request body appears empty at the time the response body is sent.
In order to get the echo behavior correct, you would need to strictly consume the request body and then send it back. Obviously this could be problematic from a memory usage standpoint if there's a large request body, but this is an inherent aspect of HTTP. If you want constant memory echo, my recommendation would be to stream the request body to a file, and then use ResponseFile for the response body.

Resources