it seems the `SinglePageAppRoutingHandler` and `Se...
# http4k
j
it seems the
SinglePageAppRoutingHandler
and
ServerFilters.Cors
filter do not play nice together. The CORS OPTION request is being handled by
SinglePageAppRoutingHandler
. Since it doesn't match a resource, the
fallbackHandler
is used with a new GET request (without headers). When that request is handled by the Cors filter, the Origin header is missing causing the request to fail...
copying the headers to the fallbackHandler's GET request fixes the issue, but I'm not sure that is the way to go
s
It'd be helpful to have an example to look at, but I'll take a guess here, anyway... Have you tried making sure the Cors filter executes before the SinglePageAppRouteHandler ? You can achieve that by casting the SPARH to
HttpHandler
so the
.then
function won't execute the filter (in this case Cors) after the routing matching attempt. Let me see how it goes. If it doesn't help, please provide an example and I'll try to help troubleshooting.
j
excellent! Casting to
HttpHandler
and wrapping it in
routes
worked.
Copy code
fun router(): (AppContext) -> HttpHandler =
    { context ->
        allRoutesFilter(context).then(
            routes(
                "/auth".let { path ->
                    AuthenticationApi.routes()(context.copy(env = context.env.with(Config.authBasePath of path))).withBasePath(path)
                },

                internalApiFilter(context).then(
                    routes(
                        "/aandachtspunten" bind AandachtspuntenApi.routes()(context),
                        "/beheer" bind BeheerApi.routes()(context),
                        "/stamdata" bind StamdataApi.routes()(context),
                        "/gebruiker" bind GebruikerApi.routes()(context),
                        "/medewerkers" bind MedewerkerApi.routes()(context),
                        "/milieubelastende-activiteiten" bind MilieubelastendeActiviteitApi.routes()(context),
                        "/milieubelastende-activiteit-types" bind MilieubelastendeActiviteitTypeApi.routes()(context),
                        "/overheidsorganisaties" bind OverheidsOrganisatieApi.routes()(context),
                        "/rechtspersonen" bind RechtspersoonApi.routes()(context),
                        "/roles" bind RolesApi.routes()(context),
                        "/kvk" bind KvkApi.routes()(context),
                        "/bag" bind BagApi.routes()(context),
                        "/zaaktype" bind ZaaktypeApi.routes()(context),
                        "/zaken" bind ZaakApi.routes()(context),
                        "/ztc" bind ZtcApi.routes()(context)
                    ).withBasePath("/internal-api")
                ),

                routes(
                    "/config" bind ConfigApi.routes(context.env)
                ).withBasePath("/public-api"),

                "/static" bind static(Classpath("/static"))
                    .withFilter(ResponseFilters.EtagSupport()),

                MockAuthenticateApi.routes(context.env).withBasePath("__authenticate"),

                routes(
                    Method.GET to (singlePageApp(extraFileExtensionToContentTypes = ContentTypes.staticContentTypes)
                        .withFilter(Filters.VueResourceCaching())
                        .withFilter(ResponseFilters.GZip()) as HttpHandler)
                ),
            )
        )
    }

fun allRoutesFilter(context: AppContext): Filter =
    ServerFilters.InitialiseRequestContext(contexts)
        .let {
            requestSlowdownInMs(context.env).let { time -> if (time > 0) it.then(Filters.SlowDownFilter(time)) else it }
        }
        .then(Filters.Cors(context))
        .then(Filters.SecurityHeaders(contentSecurityPolicy(context.env)))
        .then(Filters.UniqueRequestReference())
        .then(Filters.ReportTransactionAndError { event -> events(event) })
        .then(ErrorHandling.CatchAll(context.env))
        .then(storeAuth(context.env, context.httpClient))

fun internalApiFilter(context: AppContext): Filter =
    DebuggingFilters.PrintRequestAndResponse()
        .then(Authentication.authenticated)
        .then(DatabaseFactory.inTransaction(context.jooqConfiguration, context.datasource))
        .then(ErrorHandling.CatchLensFailure(context.env))
        .then(Authorization.storeGebruiker()(context.gebruikerRepository))
        .then(Authorization.storePermissies()(context.gebruikerRepository))
        .then(MedewerkerFilter.storeMedewerker()(context.medewerkerRepository))
Copy code
routes(
                    Method.GET to (singlePageApp(extraFileExtensionToContentTypes = ContentTypes.staticContentTypes)
                        .withFilter(Filters.VueResourceCaching())
                        .withFilter(ResponseFilters.GZip()) as HttpHandler)
                ),
s
Good to know. That’s one bit of http4k that causes confusion, because different filters may need to be applied at the correct time...
j
hmm, apparently I was too quick in my conclusion. Although the filter is called at the correct time now, the SinglePageAppRouteHandler itself doesn't work anymore...
I'll see if I can find out why
ok, in
<http://Pathmethod.to|Pathmethod.to>
the
SinglePageAppRouteHandler
is wrapped in a
TemplateRouter
which doesn't match the request spa resource
s
have you tried without the
Method.GET to
part? you should be able to just combine routinghandlers if my memory serves
j
yes, but then I'm back to the original problem, where the CORS filter is applied to late
s
Looking at the code above it seems like you need to isolate the cors + singlepage app router (casted to httphandler) into their own "routing group".
(i.e. configure cors correctly only for that handler rather than having all filters shared)
j
how do I put the singlepage app router into a separate routing group? Doesn't that make it a RoutingHttpHandler?
s
True, it does 😞 I'd need a small example to try and see how to work around this issue.
j
ok, I can make a minimal app to reproduce the behaviour. In what way/format is suitable for you?
s
the simplest way for me would be a self-contained repo. But worst case, some code snippets/gists would help too.
j
@s4nchez finally had some time to create a repo for the problem: https://github.com/jippeholwerda/http4k-cors-spa
there is a failing testcase
if you comment out the singlePageApp handler, the test passes
s
Brilliant. Thank you 🙂 I'll take a look as soon as I get a chance.
👍🏻 1
d
We've now released 4.7.0.0 with this change in.
np. On further thoughts - do we know of any reason why the SPA would need to respond to options requests? (So to return MethodNotMatched instead of Unmatched...)
Meaning you'd get a 200 with the cors headers instead of a 404 when an options request was sent to an SPA route.
(am unfamiliar which is why I'm asking)
j
I think typically you don't want an OPTIONS request to return something other than 405 Not Allowed, since it leaks sensitive information about your server's configuration
so only in the context of REST API's and CORS does it make sense to do anything with OPTIONS
we used to use nginx to serve our static images. If I send it an OPTIONS request, I also get a 405 back
so that might also be acceptable behaviour for the SPA route