Clever Tagline
Clever Tagline•13mo ago

Custom function for HTTP action?

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?
27 Replies
Convex Bot
Convex Bot•13mo ago
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 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!
ballingt
ballingt•13mo ago
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.
ballingt
ballingt•13mo ago
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.
ballingt
ballingt•13mo ago
(whcih you can run from an httpAction) But you can write your own httpAction function without too many type gymnastics
Clever Tagline
Clever TaglineOP•13mo ago
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?
Indy
Indy•13mo ago
I think we have cors helper that does this... @Tom Redman ?
Tom Redman
Tom Redman•13mo ago
Yes we have a CORS wrapper that wraps the HttpAction. It should be a good example — let me grab that link
Tom Redman
Tom Redman•13mo ago
@Clever Tagline let me know if this works as an example of wrapping the HttpAction: https://github.com/tomredman/convex-cors/blob/main/convex/helpers/corsHttpRouter.ts
GitHub
convex-cors/convex/helpers/corsHttpRouter.ts at main Ā· tomredman/co...
Contribute to tomredman/convex-cors development by creating an account on GitHub.
Tom Redman
Tom Redman•13mo ago
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.
Clever Tagline
Clever TaglineOP•13mo ago
@Indy @Tom Redman Thank you! I'll dig through that and let you know if I have any more questions. 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
})
})
}
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});
})
)
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?
ballingt
ballingt•13mo ago
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.
Clever Tagline
Clever TaglineOP•13mo ago
Good point. šŸ‘
Clever Tagline
Clever TaglineOP•3mo ago
@ballingt 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);
})
}
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.
No description
ballingt
ballingt•3mo ago
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
convex-js/CHANGELOG.md at main Ā· get-convex/convex-js
TypeScript/JavaScript client library for Convex. Contribute to get-convex/convex-js development by creating an account on GitHub.
Clever Tagline
Clever TaglineOP•3mo ago
Thanks. If PublicHttpAction is no longer callable, then clearly I updated the package at some point, but it's obviously been a while as my version is way behind. I'll try your suggestion and let you know if I hit any snags.
ballingt
ballingt•3mo ago
If you need a quick fix, in the latest package there are two private (you won't have types for them, and they're not stable, but they'll work for now) ways to call an http action: https://github.com/get-convex/convex-js/blob/ff3a40c2116f1a0857c1993b95a347e6f844ed1c/src/server/registration.ts#L437-L440 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
Clever Tagline
Clever TaglineOP•3mo ago
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);
})
}
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
})
)
// 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
ballingt
ballingt•3mo ago
Ah I don't think you should use httpAction in the inner part
Clever Tagline
Clever TaglineOP•3mo ago
Sorry. I think I misinterpreted your comment above:
I'd build as a a function authCheckHttpAction(inner: (ctx, Request) => Response) today instead
ballingt
ballingt•3mo ago
just a sec, I'll write one
Clever Tagline
Clever TaglineOP•3mo ago
Thanks. Sorry for taking your time on this. @ballingt Hang on. I'm getting an idea. Let me see if I can work this out without taking your time.
ballingt
ballingt•3mo ago
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);
});
};
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")!);
});
};
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")!);
});
};
Clever Tagline
Clever TaglineOP•3mo ago
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.
ballingt
ballingt•3mo ago
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
Clever Tagline
Clever TaglineOP•3mo ago
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.
ballingt
ballingt•3mo ago
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
Clever Tagline
Clever TaglineOP•3mo ago
...I'm not following you. But that's okay. My head feels awful today, so I’m going to stop while I've got a working thing and move on.

Did you find this page helpful?