#Custom function for HTTP action?

40 messages · Page 1 of 1 (latest)

pine sandal
#

I want to use the same authorization process for all HTTP actions in a project rather than including the auth code in each action. I thought a custom action might work, but after reading the Stack post about custom functions, and also reviewing the code in the convex-helpers package, HTTP actions don't appear to be supported. Am I reading that correctly?

sweet sableBOT
#

Thanks for posting in #1088161997662724167.
Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets.

    - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.)
    - Use [search.convex.dev](https://search.convex.dev) to search Docs, Stack, and Discord all at once.
    - Additionally, you can post your questions in the Convex Community's #1228095053885476985 channel to receive a response from AI.
    - Avoid tagging staff unless specifically instructed.

    Thank you!
wispy obsidian
#

That's right, you'd need to do something similar. It's not as messy as custom functions because there's less going on, you can wrap a httpAction without changing as much.

#

Also once you get deep into this you might want to jump to something like https://hono.dev/

Hono is a small, simple, and ultrafast web framework built on Web Standards. It works on Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js. Fast, but not only fast.

#

(whcih you can run from an httpAction)

#

But you can write your own httpAction function without too many type gymnastics

pine sandal
#

Thanks. I don't see us needing that many HTTP actions for our use case, so I probably won't need to go as far as Hono.

#

As for how to wrap httpAction, are there any examples on how to do that?

#

Or should I just review how it's handled for the other functions in convex-helpers?

crude minnow
#

I think we have cors helper that does this... @delicate oracle ?

delicate oracle
#

Yes we have a CORS wrapper that wraps the HttpAction. It should be a good example — let me grab that link

#

This wraps the router, but the pattern should be similar. Can dig in more a little later!

#

Line 290 has an example of wrapping an action.

pine sandal
#

@crude minnow @delicate oracle Thank you! I'll dig through that and let you know if I have any more questions.

pine sandal
#

I got it working. This was a new experience for me, so maybe there's something I could do better, but here's how I implemented it in case it can be useful for others.

Here's my wrapper function (simplified):

import { PublicHttpAction } from "convex/server";
import { httpAction } from "./_generated/server"

export const authCheck = (originalHandler: PublicHttpAction) => {
  return httpAction(async (_, request) => {

    // ... Validate the request ...
    // Responses with error codes are returned if validation fails for various reasons

    // If we got this far, execute the original handler
    let originalResponse = await originalHandler(_, request);

    return new Response(originalResponse.body, {
      status: originalResponse.status,
      statusText: originalResponse.statusText,
      headers: originalResponse.headers
    })
  })
}

I tested the wrapping behavior like so:

export const authTest = authCheck(
  httpAction(async (ctx, request) => {
    // ... core action logic ...
    return new Response("OK", {status: 200});
  })
)

In the http.ts file, authTest was set as the handler on the desired route.

I tested with both valid and invalid credentials, and it performed as desired. 👍

Being new to writing wrapper functions, my only question is re: the function design. My original version accepted an object with a originalHandler property (following the example in the code you shared), but then I simplified it to just accept a single argument, as using the object felt like unnecessary complexity for my use case. Is there a benefit to doing it one way vs the other?

wispy obsidian
#

Looks good! I would probably return the original response directly instead of creating a new one, in case there are other things beisdes status, statusText, headers, and body on it.

#

Simplified seesm great. The versions in convex-helpers are sometimes more general because we want devs to be able to do anything with them, but I wouldn't make your own verison more general than it needs to be.

pine sandal
#

@wispy obsidian Resurrecting this thread as I'm being alerted to a type issue, and I don't understand why.

Here's the current state of the wrapper:

import { PublicHttpAction } from "convex/server";
import { httpAction } from "./_generated/server"

export const httpAuthCheck = (originalHandler: PublicHttpAction) => {
  return httpAction(async (_, request) => {
    // ... Validate the request ...
    // Responses with error codes are returned if validation fails for various reasons
    // This is all still working

    // If we got this far, execute the original handler and return its response
    return await originalHandler(_, request);
  })
}

I haven't opened the file in a while, and haven't been notified of any errors, so I believe it's still working. However, the originalHandler is now flagged with a type error (see screenshot)

Any ideas why it would do this? I haven't updated the Convex package in my repo in a while, and I'm still on 1.16.3. Do you know if an update would fix this? I couldn't find anything in the changelog specifically related to this issue.

wispy obsidian
# pine sandal <@524424754405572617> Resurrecting this thread as I'm being alerted to a type is...

strange if you haven't updated anything! PublicHttpAction is indeed no longer callable, this was dropped around the same time that calling mutations, queries, and actions directly was disabled. https://github.com/get-convex/convex-js/blob/main/CHANGELOG.md#1180

I'd build as a a function authCheckHttpAction(inner: (ctx, Request) => Response) today instead

GitHub

TypeScript/JavaScript client library for Convex. Contribute to get-convex/convex-js development by creating an account on GitHub.

pine sandal
wispy obsidian
#

but in general we don't want these to be callable so that it's not confusing for things like whether middleware or arg or return validators run or not when these are called directly, we want httpAction to be the outermost layer

pine sandal
# wispy obsidian If you need a quick fix, in the latest package there are two private (you won't ...

Gotcha. I don't mind going the route that you first suggested, but I'll admit that I don't completely understand it now that I'm actually trying to update the code. Here's where I'm at so far:

export const authCheckHttpAction = (inner: (ctx: GenericActionCtx<any>, request: Request) => Promise<Response>) => {
  return httpAction(async (ctx, request) => {
    // Same innards as the original version

    // If we got this far, execute the original handler and return its response
    return await inner(ctx, request);
  })
}

With that done, do I just change everything that uses the old function to use this new one instead? E.g.

// Current
export const someAction = httpAuthCheck(
  httpAction(async (ctx, request) => {
    // action logic here
  })
)

// New
export const someAction = authCheckHttpAction(
  httpAction(async (ctx, request) => {
    // action logic here
  })
)

Am I understanding that correctly?

#

Now that I've typed all that out, it doesn't look much different than the original. Clearly something isn't clicking, and I don't know what it is...

#

I also tried to test the private invokeHttpAction method after updating to the latest version of Convex, but I'm getting an error that the property doesn't exist.

#

Not sure where to go from here

wispy obsidian
#

Ah I don't think you should use httpAction in the inner part

pine sandal
#

Sorry. I think I misinterpreted your comment above:

I'd build as a a function authCheckHttpAction(inner: (ctx, Request) => Response) today instead

wispy obsidian
#

just a sec, I'll write one

pine sandal
#

Thanks. Sorry for taking your time on this.

#

@wispy obsidian Hang on. I'm getting an idea. Let me see if I can work this out without taking your time.

wispy obsidian
#

haven't tried it but this is what I'm thinking

const myHttpAction = (
  func: (ctx: ActionCtx, request: Request) => Promise<Response>,
): PublicHttpAction => {
  return httpAction(async (ctx: GenericActionCtx<any>, request: Request) => {
    if (request.headers.get("Authorization") !== "secret") {
      return new Response("no!", { status: 400, statusText: "Missing Secret" });
    }
    return func(ctx, request);
  });
};
#

this also lets you customize the interface if you want

const mySpecialHttpAction = (
  func: (ctx: ActionCtx, request: Request, secret: string) => Promise<Response>,
): PublicHttpAction => {
  return httpAction(async (ctx: GenericActionCtx<any>, request: Request) => {
    if (request.headers.get("Authorization") !== "secret") {
      return new Response("no!", {
        status: 400,
        statusText: "Missing Secret",
      });
    }
    return func(ctx, request, request.headers.get("Authorization")!);
  });
};
pine sandal
#

Thanks. That looks promising, and definitely better than my approach. While I understand what this does, this isn't the kind of thing that my mind naturally goes to, and I'm not sure what it'll take to get to that point.

wispy obsidian
#

totally, this is in line with the middleware in convex-helpers, it helps to see a pattern; we should put this in docs as an example.

#

It can also be nice to outsource this all to Hono, which we need an example of too; if you want to live in that ecosystem it can be nice to have a fully-featured router instead of the clear-but-simple convex one

pine sandal
#

Eh...I'm not sure if I want to live in that ecosystem, frankly. Servers intimidate me. I'm happy to solve problems at the data and application levels, but I prefer to leave deeper stuff like server ops to more skilled folks.

wispy obsidian
#

Oh I was thinking using Hono in Convex; same Convex-deals-with-servers-for-me setup, but using a different JavaScript library to do the routing