Way too often I saw myself and my colleagues write huge, very hard to understand deeply nested if () then else code to handle any control path but the complete success one in RestXQ code. [1] is an example of such code even though it uses the module I want to introduce here. It seems I didn’t have time to refactor it yet. To add insult to injury the code producing a 500, 404, 403, 401, 302 or maybe even 201 response code was * neither short nor very uniform and * would respond to the browser with XML/XHTML or JSON or text but in most situations the format the browser side had the hardest time to handle
For example [2] delivers an XHTML message no matter what the Accept header says and although it is an accurate message to a user, very similar XML snippets are found throughout that unit and maybe need to be adapted each separately if the HTML needs to updated.
My XML snippets CRUD API started as a port of an apigility (now laminas api-tools [3]) API working with a relational backend to store XML snippets to something that does the same but just uses BaseX for any data storage and is much better at querying using sophisticated XPaths. So from that previous API I learned about RFC 7807. [4]
RFC 7807 “Problem Details for HTTP APIs” [5] is one of several specification now available for reporting errors from REST APIs. This specification explicitly states how the errors should look like in XML as well as in JSON.
Maybe this is a bit bold, but I used the URL of that very RFC 7807 as the resolved URI for my module. [6] Please note: I have to admit my modules have something unusual in common: I use the namespace prefix “_” within the module and only use a more talking prefix when I use a module elsewhere. So, the “_” prefix can map to numerous URIs in my code. I am not 100% sure if there are down sides to this style but I use it for a while now and no problems come to mind.
I really dislike to get an error, especially during development of some service, without any indication of where that actually occurred. That is to say: I like stack traces in my errors. It also would be great if any runtime error in my code would be reported as XML or JSON, depending on the format the browser asked for, just as errors I explicitly raise.
A few parts provided by BaseX greatly help in getting all of this packaged in some xqm-file. * A stack trace is always available in “$err:additional” when catching errors [7] * One catch-all error handler can be installed (“declare %rest:error('*') function”, although there are minor downsides to this catch-all handler) [8] * The XML based direct format BaseX uses to store JSON by default makes it very easy to transform RFC 7807 XML to JSON [9] * It is easy to query the request header anywhere in RestXQ XQuery code running on BaseX [10]
I tried to have easy to remember function names that make the code readable as if it was a sentence. Therefore for example, I created a function wrapping the users code that says “return api-problem:or-result(user_function#x, [params, …])”[11]. Another example would be “return api-problem:result(<problem>[…]</problem>)”.
I also wanted to come up with an “intuitive way” to send standard HTTP response codes. What I came up with is a special namespace and a mapping of status codes to standard messages that is part of my module. So for example a status code 404 can be returned like this: “error(xs:QName('response-codes:_404'), $api-problem:codes_to_message(404), 'A custom message')” [12]. Something similar is probably possible with “web:response-header()” but with the error function something like if
let $check_file_exists := if (not(file:exists($path))) then error(xs:QName('response-codes:_404'), $api-problem:codes_to_message(404), 'A custom message')
at the top of a more complex RestXQ function is possible. There is no need to wrap custom code in one or several “if () then else” blocks. This helps readability in my opinion very much, especially if you have to check quite a few things before, say, writing something to the database.
The idea also works great with permission checks that use %perm:check annotations. My first use of my api-problem module predates the addition of these helpful annotations. [13]
A while before we had a HTTP header parameter that gave us the execution time, I wanted to measure execution time. So the functions take an xs:integer that should be obtained using “prof:current-ns()” [14] as the very first variable in a RestXQ function and I try to execute a second “prof:current-ns()” as late as I can imagine in my module [15]. That way I think I get a reasonably accurate timing result in my outputs as long as the respective error does not come from the catch-all handler.
I also incorporated a quick and dirty HTML page rendering function that displays an error in detail if someone needs that [16]. As a small addition this error page can link to error descriptions in the W3C standards. [17]
By the way: developing this started on BaseX but then we had similar needs in another open-source XML database existing today so I tried to port the code. Not only worked that rather well (probably also because I know the XQuery needed there rather well too), I also had a few new ideas and added them back to the BaseX version. May be there is some left over code in the module at the moment that reimplements some helpful, non-standard XQuery functions for this reason. The code is somewhat portable.
A few thoughts: * Some say that it is necessary for security purposes to disable any stack traces in production environments. I am not really believing this does much good. But if one does not want to hard code a Boolean switch in the modules source code: What would be the fastest external source one could use in terms of compile, optimizing and execution time? Are stack traces not available as “$err:additional” when RESTXQERRORS are switched off? * Is there any sane way to get a QName with an unknown prefix of an error as a string like in the catch all handler and resolve it against all prefix-URI mappings known in some XQuery program?
[1] https://github.com/acdh-oeaw/vleserver_basex/blob/main/vleserver/users.xqm#L... [2] https://github.com/acdh-oeaw/vicav-app/blob/master/http.xqm#L24-L55 [3] https://api-tools.getlaminas.org/ [4] https://api-tools.getlaminas.org/documentation/modules/api-tools-api-problem [5] https://datatracker.ietf.org/doc/html/rfc7807 [6] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/api-problem/api-... [7] https://docs.basex.org/wiki/XQuery_3.0#Try.2FCatch [8] https://docs.basex.org/wiki/RESTXQ#Catch_XQuery_Errors [9] https://docs.basex.org/wiki/JSON_Module#Direct [10] https://docs.basex.org/wiki/Request_Module#request:header [11] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/api-proble... [12] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/http.xqm#L... [13] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/http.xqm#L... [14] https://github.com/acdh-oeaw/vleserver_basex/blob/main/vleserver/dicts.xqm#L... [15] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/api-problem/api-... [16] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/api-proble... [17] https://github.com/acdh-oeaw/api-problem4restxq/blob/master/api-problem/api-...
Best regards