Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RESTXQ Multiple Path Annotations #3405

Open
ahenket opened this issue May 7, 2020 · 3 comments
Open

RESTXQ Multiple Path Annotations #3405

ahenket opened this issue May 7, 2020 · 3 comments
Assignees
Labels
discuss ask for feedback

Comments

@ahenket
Copy link

ahenket commented May 7, 2020

Describe the bug
I got around to testing multiple %rest:path annotations to support my need for different capitalizations on the same path. Results:

  • As long as I have 1 function that switches based on Accept header to produce output, it will support multiple %rest:path annotations that differ only on capitalization.
  • However when you have multiple functions, each handling a different Accept type, you will only get HTTP 405 once you add multiple %rest:path annotations that differ only in capitalization.

Tester code attached. api/test1 and api/Test1 will work. api/test2 nor api/Test2 works, unless you remove the lower-case or upper-case paths.

Expected behavior
I expect both styles to work the same. E.g. api/Test2 should be sent to testapi:getTestJson().

To Reproduce
Add code below to a location in eXist-db where RestXQ is triggered. Make sure it has execution permissions for guest.

Go to postman or browser and run:

  • api/test1 or api/Test1 (both will work)
  • api/test2 or api/Test2 (don't work, unless you remove the lower-case or upper-case paths)
xquery version "3.1";

(: http://exquery.github.io/exquery/exquery-restxq-specification/restxq-1.0-specification.html :)

module namespace testapi            = "http://art-decor.org/ns/art-decor/api/test";

declare namespace json              = "http://www.json.org";
declare namespace expath            = "http://expath.org/ns/pkg";

declare namespace rest              = "http://exquery.org/ns/restxq";
declare namespace req               = "http://exquery.org/ns/request";
declare namespace http              = "http://expath.org/ns/http-client";
declare namespace output            = "http://www.w3.org/2010/xslt-xquery-serialization";

declare 
    %rest:GET 
    %rest:path("/api/test1")
    %rest:path("/api/Test1")
function testapi:getTestDynamic() {
let $mimetype   := local:getMimeType()
let $method     := local:getMethodFromMimeType($mimetype)

return (
    <rest:response>
        <http:response>
            <http:header name="Content-Type" value="{$mimetype}"/>
        </http:response>
        <output:serialization-parameters>
            <output:method value="{$method}"/>
            <output:media-type value="{$mimetype}"/>
        {   if ($method = 'json') then
                <exist:json-ignore-whitespace-text-nodes value="yes"/>
            else ()
        }
        </output:serialization-parameters>
    </rest:response>
    ,
    <serverinfo accept="{req:header("Accept")}" method="{$method}" mimetype="{$mimetype}">
        <desc language="en-US"/>
        <database version="{system:get-version()}"/>
    </serverinfo>
)
};

declare 
    %rest:GET 
    %rest:path("/api/test2")
    %rest:path("/api/Test2")
    %rest:produces("application/json", "text/json")
    %output:media-type("text/json")
    %output:method("json")
    %exist:serialize("json-ignore-whitespace-text-nodes=yes")
function testapi:getTestJson() {
    <serverinfo>
        <database version="{system:get-version()}"/>
    </serverinfo>
};

declare 
    %rest:GET 
    %rest:path("/api/test2")
    %rest:path("/api/Test2")
    %rest:produces("application/xml", "text/xml")
    %output:media-type("text/xml")
    %output:method("xml")
function testapi:getTestXml() {
    <serverinfo>
        <database version="{system:get-version()}"/>
    </serverinfo>
};

(:~ Determine which of the accepted mime-types is to be used here

    Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
:)
declare %private function local:getMimeType() as xs:string? {
let $defaultmimetype    := 'text/json'
let $mimetype           := tokenize(normalize-space(tokenize(req:header("Accept"), ';')[1]), ',')

return
    if ($mimetype[. = 'application/json']) then 'application/json' else
    if ($mimetype[. = 'text/json']) then 'text/json' else
    if ($mimetype[. = '*/json']) then 'text/json' else
    if ($mimetype[. = 'application/xml']) then 'application/xml' else
    if ($mimetype[. = 'text/xml']) then 'text/xml' else
    if ($mimetype[. = '*/xml']) then 'text/xml' else
    if ($mimetype[. = 'text/html']) then 'text/html' else
    if ($mimetype[. = 'text/plain']) then 'text/plain' else
    if ($mimetype[. = '*/*']) then $defaultmimetype
    else ()
};

declare %private function local:getMethodFromMimeType($mimetype as xs:string?) as xs:string? {
    switch ($mimetype)
    case 'application/xml' return 'xml'
    case 'text/xml' return 'xml'
    case 'application/json' return 'json'
    case 'text/json' return 'json'
    case 'text/html' return 'html'
    case 'text/plain' return 'text'
    default return ()
};

Context (please always complete the following information):

  • OS: macOS 10.15.4
  • eXist-db version: 5.2.2
  • Java(TM) SE Runtime Environment (build 1.8.0_121-b13) 64bit

Additional context

  • dmg install
@triage-new-issues triage-new-issues bot added the triage issue needs to be investigated label May 7, 2020
@joewiz
Copy link
Member

joewiz commented May 7, 2020

Is one spelling of each endpoint canonical? If so, why not handle the non-canonical variants with a 302 permanent redirect to the canonical endpoint?

@ahenket
Copy link
Author

ahenket commented May 7, 2020

That could be a workaround. But I think there should no difference between the two styles of handling the Accept. For Accept headers to work correctly I’d need an additional copy for every Spelling * Accept or add a controller for it. Keeping it in 1 solution lessens the risk of going out of sync

@adamretter
Copy link
Member

adamretter commented May 8, 2020

Hi @ahenket thanks for reporting. I thought I would remind myself of the intended behaviour that I set out in the initial RESTXQ paper. I found the following two parts that are relevant in section 4.3.1:

A Resource Function must contain a single path annotation.

Whilst having multiple %rest:path annotations isn't raising an error (maybe it should be?), it seems that probably chooses the last one. For example:

$ curl -X GET -I http://localhost:4059/exist/restxq/api/test1
HTTP/1.1 405 Method Not Allowed
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 576
Server: FDB(1.0.0-SNAPSHOT)
$ curl -X GET -I http://localhost:4059/exist/restxq/api/Test1
HTTP/1.1 200 OK
Date: Fri, 08 May 2020 12:00:41 GMT
Content-Type: application/xml; charset=UTF-8
Vary: Accept-Encoding, User-Agent
Transfer-Encoding: chunked
Server: FDB(1.0.0-SNAPSHOT)

IMHO it seems like having multiple path annotations should raise the error RQST0001 in the same way that: %rest:path("/api/test1", "/api/Test1") does.

We could however think about improving on this behaviour of RESTXQ 1.0 in say RESTXQ 2.0 if people are interested in creating a 2.0 spec?

When many URI paths are defined, conflicts may occur. It is implementation defined how these
should be resolved.

That's not very helpful is it!

Both %rest:path and %rest:produces are constraints, which means that they can be used them to determine which function should service the incoming requests.

By running rest:resource-functions()/rest:resource-function[@xquery-uri eq "/db/testapi.xqm"] we can see what Resource Functions have been discovered after you save your XQuery module.

For your Test2 example, if I remove the multiple path annotations as we know that isn't supported, I get something like this:

declare 
    %rest:GET 
    %rest:path("/api/Test2")
    %rest:produces("application/json", "text/json")
    %output:media-type("text/json")
    %output:method("json")
    %exist:serialize("json-ignore-whitespace-text-nodes=yes")
function testapi:getTestJson() {
    <serverinfo>
        <getTestJson>true</getTestJson>
        <database version="{system:get-version()}"/>
    </serverinfo>
};

declare 
    %rest:GET 
    %rest:path("/api/Test2")
    %rest:produces("application/xml", "text/xml")
    %output:media-type("text/xml")
    %output:method("xml")
function testapi:getTestXml() {
    <serverinfo>
        <getTestXml>true</getTestXml>
        <database version="{system:get-version()}"/>
    </serverinfo>
};

And rest:resource-functions() reports:

<rest:resource-function xmlns:rest="http://exquery.org/ns/restxq" xquery-uri="/db/testapi.xqm">
    <rest:identity namespace="http://testapi" local-name="getTestJson" arity="0"/>
    <rest:annotations>
        <rest:GET/>
        <rest:produces>
            <rest:internet-media-type>application/json</rest:internet-media-type>
            <rest:internet-media-type>text/json</rest:internet-media-type>
        </rest:produces>
        <rest:path specificity-metric="7">
            <rest:segment>api</rest:segment>
            <rest:segment>Test2</rest:segment>
        </rest:path>
        <output:media-type xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">text/json</output:media-type>
        <output:method xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">json</output:method>
    </rest:annotations>
</rest:resource-function>

<rest:resource-function xmlns:rest="http://exquery.org/ns/restxq" xquery-uri="/db/testapi.xqm">
    <rest:identity namespace="http://testapi" local-name="getTestXml" arity="0"/>
    <rest:annotations>
        <rest:GET/>
        <rest:produces>
            <rest:internet-media-type>application/xml</rest:internet-media-type>
            <rest:internet-media-type>text/xml</rest:internet-media-type>
        </rest:produces>
        <rest:path specificity-metric="7">
            <rest:segment>api</rest:segment>
            <rest:segment>Test2</rest:segment>
        </rest:path>
        <output:media-type xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">text/xml</output:media-type>
        <output:method xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">xml</output:method>
    </rest:annotations>
</rest:resource-function>

We can see that we can call both of these resource functions using curl:

$ curl -X GET -H 'Accept: application/json' -I http://localhost:4059/exist/restxq/api/Test2
HTTP/1.1 200 OK
Date: Fri, 08 May 2020 12:16:28 GMT
Content-Type: text/json;charset=utf-8
Vary: Accept-Encoding, User-Agent
Transfer-Encoding: chunked
Server: FDB(1.0.0-SNAPSHOT)
$ curl -X GET -H 'Accept: application/xml' -I http://localhost:4059/exist/restxq/api/Test2
HTTP/1.1 200 OK
Date: Fri, 08 May 2020 12:17:11 GMT
Content-Type: text/xml;charset=utf-8
Vary: Accept-Encoding, User-Agent
Transfer-Encoding: chunked
Server: FDB(1.0.0-SNAPSHOT)

So I think the problem is not with the %rest:produces constraints but rather with your use of multiple %rest:path annotations on a single function. I am afraid that I may have mislead you by suggesting that was possible initially.

I think it would be nice though if multiple %rest:path annotations were allowed...

@adamretter adamretter self-assigned this May 8, 2020
@adamretter adamretter changed the title [BUG] RESTXQ Multiple Path Annotations May 8, 2020
@adamretter adamretter added the discuss ask for feedback label May 8, 2020
@triage-new-issues triage-new-issues bot removed the triage issue needs to be investigated label May 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss ask for feedback
Projects
None yet
Development

No branches or pull requests

3 participants