SWI-Prolog / packages-http

The SWI-Prolog HTTP server and client libraries
23 stars 23 forks source link

HTTP library doesn't seem to be able to issue arbitrary documents with non-200 status messages #132

Closed thetrime closed 5 years ago

thetrime commented 5 years ago

I'm trying to implement an API where the other party expects to get HTTP statuses like 403 when they ask for a resource they are not entitled to see, or 404 when they ask for a resource that does not exist, or a 501 when they ask for a method I have not implemented. They'll be sending a JSON document in a POST or issuing a GET request with parameters encoded in the URL.

While I can easily generate a 404 response, I seemingly can't control the content of the page, and specifically, I can't generate a 404 page with a content-type other than text/html. I want to be able to call a predicate like rejectmessage(404, {reason: 'This object does not exists', reference: MyInternalReference}), have it throw(http_reply()) and then generate a 404 page with my document as the body.

The hook http:map_exception_to_http_status_hook/4 lets us map an arbitrary exception to a new term, so I first tried using this hook to map my exception term to something that would end up in status_reply/3. Then I lose control a bit - in order to get a reply with content, status_has_content/1 has to succeed for my term, which limits me to 12 possibilities (excluding, for example, 501 Not Implemented). status_page_hook/3 is then called, and that looks up the status code from the functor name. I could then hook http:status_reply/3 to generate a body/3 with my JSON document in it.

This all seems pretty complicated at this point and I'm starting to wonder if I'm missing an obvious alternative approach. Is this the right approach, and is there missing functionality that should be implemented here to allow us to emit a document directly? By far the simplest option is to add a hook so that I can control http_reply/6 directly, like

:-multifile(http:http_reply_hook/6).
http_reply(Data, Out, HdrExtra, Context, Request, Code) :-
   http:http_reply_hook(Data, Out, HdrExtra, Context, Request, Code).

Then I can simply throw http_reply(my_custom_object(....)) and write a hook to handle it along the lines of (obviously this needs a bit of polish)

:-multifile(http:http_reply_hook/6).
http:http_reply_hook(my_custom_object(StatusCode, Data), Out, HdrExtra, Context, Request, Code):-
       phrase(reply_header(status(StatusCode), HdrExtra, StatusCode), Header),
       format(Out, '~s', [Header]),
       format(Out, 'Content-type: application/json~n~n', []),
       json_write_dict(Out, Data, []).

Does that seem even remotely reasonable?

JanWielemaker commented 5 years ago

You missed the obvious:

format('Status: 501~n')

as part of the header. That allows you to send any document with any header. The entire status stuff quite complicated, mostly due to a far too much incremental design :cry: Properly configured though, it simply allows the real code to throw an exception.

thetrime commented 5 years ago

That was worth asking :)