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#L90-L152
[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-problem.xqm#L3
[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-problem-rest-test.xqm#L21
[12]
https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/http.xqm#L44
[13]
https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/http.xqm#L100-L105
[14]
https://github.com/acdh-oeaw/vleserver_basex/blob/main/vleserver/dicts.xqm#L47-L51
[15]
https://github.com/acdh-oeaw/api-problem4restxq/blob/master/api-problem/api-problem.xqm#L179-L188
[16]
https://github.com/acdh-oeaw/api-problem4restxq/blob/master/tests/api-problem-rest-test.xqm#L20
[17]
https://github.com/acdh-oeaw/api-problem4restxq/blob/master/api-problem/api-problem.xqm#L487
Best regards
--
Mag. Ing. Omar Siam
Austrian Center for Digital Humanities and Cultural Heritage
Österreichische Akademie der Wissenschaften | Austrian Academy of Sciences
Stellvertretende Behindertenvertrauensperson | Deputy representative for disabled persons
Wohllebengasse 12-14, 1040 Wien, Österreich | Vienna, Austria
T: +43 1 51581-7295
omar.siam@oeaw.ac.at | www.oeaw.ac.at/acdh