jollytoad / deno_http_fns

A bunch of functions for building HTTP servers
https://jsr.io/@http
MIT License
18 stars 1 forks source link

CORS question #16

Closed NetOpWibby closed 3 months ago

NetOpWibby commented 3 months ago

Hey jollytoad,

I stumbled upon your repo literally yesterday and your cors module has been most helpful in production. It's addition made local development unworkable because idk wtf I'm doing, quite honestly.

I have two servers, api and app. The api is Deno and is using cors. The app is SvelteKit and is not. The app requests from the api often. Here's how I have my Deno server setup:

Deno.serve({
  handler: intercept(async(req) => {
    const { pathname } = new URL(req.url);

    return pathname === "/graphql" ?
      await GraphQLHTTP<Request>({
        context: request => ({
          request,
          "x-session": new Headers(req.headers).get("authorization")
        }),
        graphiql: true, // isDevelopment,
        schema
      })(req) :
      Response.json({
        detail: "Please visit our documentation for information on how to use the beachfront/ API.",
        status: 406,
        title: "Not Acceptable",
        url: "https://domains.beachfront/kb/developer"
      });
  }, cors({ allowHeaders: "*", allowMethods: "*", allowOrigin: "*" })),
  hostname: "0.0.0.0",
  onListen({ port }) {
    // pretty console message
  },
  port
}) as Deno.HttpServer;

Any ideas on what I could be doing wrong? In production, my server looks like this:

Deno.serve({
  handler: async(req) => {
    const { pathname } = new URL(req.url);

    return pathname === "/graphql" ?
      await GraphQLHTTP<Request>({
        context: request => ({
          request,
          "x-session": new Headers(req.headers).get("authorization")
        }),
        graphiql: true, // isDevelopment,
        schema
      })(req) :
      Response.json({
        detail: "Please visit our documentation for information on how to use the beachfront/ API.",
        status: 406,
        title: "Not Acceptable",
        url: "https://domains.beachfront/kb/developer"
      });
  },
  hostname: "0.0.0.0",
  onListen({ port }) {
    // pretty console message
  },
  port
}, cors());

I changed stuff around in dev while troubleshooting.

In production, my api server also has a Caddyfile that looks like this:

api.beachfront.domains {
  reverse_proxy localhost:2413

  route /graphql {
    header {
      Access-Control-Allow-Credentials true
      Access-Control-Allow-Headers *
      Access-Control-Allow-Methods *
      Access-Control-Allow-Origin *
    }

    @options {
      method OPTIONS
    }

    respond @options 204
  }

  route /ui {
    reverse_proxy localhost:10700/ui
  }
}
NetOpWibby commented 3 months ago

Looking at your module, it looks like it does send the correct response to an OPTIONS request. Idk what's going on.

Access to fetch at 'API' from origin 'APP' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

jollytoad commented 3 months ago

In its current state the cors interceptor just modifies headers, it doesn't change the status or body of the response. So your code appears to just handle the request exactly the same for every method, even OPTIONS, returning whatever the graphql fn does or the 406 status.

You need to return a 200 OK (or usually 204 No Content) for a OPTIONS request.

You can do this yourself in your handler, or use the byMethod helper which will automatically handle OPTIONS (and HEAD) appropriately.

Thanks for bringing this up, I generally always use byMethod to handle all requests, so I guess I've not hit this problem myself.

I think I need to provide a better example and docs for cors, and/or handle OPTIONS differently. I really need to get some browser-based tests setup for it too.

Here's an example using other router functions from the @http lib:

import { intercept } from "jsr:@http/interceptor/intercept";
import { byMethod } from "jsr:@http/route/by-method";
import { byPattern } from "jsr:@http/route/by-pattern";
import { withFallback } from "jsr:@http/route/with-fallback";
import { cors } from "jsr:@http/interceptor/cors";

Deno.serve({
  handler: intercept(
    withFallback(
      byPattern("/graphql",
        byMethod({
          GET: handleGraphQLRequest,
          // I don't know what methods graphql supports, but you'll need to list
          // them all here pointing at the same handler fn.
          PUT: handleGraphQLRequest, 
        })
      ),
      handleFallbackRequest
    ),
    cors()
  )
});

function handleGraphQLRequest(req: Request) {
  // do your graphql call here
}

function handleFallbackRequest(req: Request) {
  // return your appropriate not-found etc response
}
NetOpWibby commented 3 months ago

Thank you for getting me back on track! Here's how I solved it:

import { STATUS_CODE } from "https://deno.land/std@0.224.0/http/status.ts";

...
    if (req.method === "OPTIONS") {
      return new Response(null, {
        status: STATUS_CODE.NoContent
      });
    }
...

Unsure if I need cors({ allowOrigin: isDevelopment ? "*" : "https://beachfront.domains" }), still testing. I was able to remove the CORS stuff from my Caddyfile though.