How to send a custom response by throwing an error and catching it
This method is useful in case you want to send a custom response from an overrided function which doesn't have access to the response object. This can happen in case you are overriding any of our recipe interface functions.
Let's take an example in which we want to prevent a user from logging into a new device if a session already exists for them in another device. We also want to send a custom response from the API in case we are preventing a login. For this, we will be overriding the createNewSession
function from the Session recipe.
In the override, we will check if a session already exists for the given userId, and if it does, we will throw an error from the function. This error will be propogated to your application's error handler (or an error handler callback if you have provided one to us), in which you can catch this and send a custom response.
#
Step 1First, we override the createNewSession
function and throw an error in case a session already exists for a user. We can do this in the Session.init
function:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import Session from "supertokens-node/recipe/session";
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
createNewSession: async function (input) {
const existingSessions = await Session.getAllSessionHandlesForUser(input.userId);
if (existingSessions.length > 0) {
// this means that the user already has a session on some other device
throw new Error("Session already exists on another device");
}
// no other session exists, and so we can continue with logging in this user
return originalImplementation.createNewSession(input);
}
}
}
}
})
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
session.Init(&sessmodels.TypeInput{
Override: &sessmodels.OverrideStruct{
Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
// first we copy the original implementation
originalCreateNewSession := *originalImplementation.CreateNewSession
(*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) {
existingSessions, err := session.GetAllSessionHandlesForUser(userID, &tenantId, userContext)
if err != nil {
return nil, err
}
if len(existingSessions) > 0 {
// this means that the user already has a session on some other device
return nil, errors.New("Session already exists on another device")
}
// no other session exists, and so we can continue with logging in this user
return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext)
}
return originalImplementation
},
},
})
}
from supertokens_python.recipe.session.asyncio import get_all_session_handles_for_user
from supertokens_python.recipe import session
from supertokens_python.recipe.session.interfaces import RecipeInterface
from typing import Any, Dict, Optional
from supertokens_python.types import RecipeUserId
def override_session_functions(original_implementation: RecipeInterface):
# first we copy the original implementation
original_create_new_session = original_implementation.create_new_session
async def create_new_session(
user_id: str,
recipe_user_id: RecipeUserId,
access_token_payload: Optional[Dict[str, Any]],
session_data_in_database: Optional[Dict[str, Any]],
disable_anti_csrf: Optional[bool],
tenant_id: str,
user_context: Dict[str, Any],
):
existing_sessions = await get_all_session_handles_for_user(user_id)
if len(existing_sessions) > 0:
# this means that the user already has a session on some other device
raise Exception("Session already exists on another device")
# no other session exists, and so we can continue with logging in this user
return await original_create_new_session(
user_id,
recipe_user_id,
access_token_payload,
session_data_in_database,
disable_anti_csrf,
tenant_id,
user_context,
)
original_implementation.create_new_session = create_new_session
return original_implementation
session.init(override=session.InputOverrideConfig(functions=override_session_functions))
#
Step 2Next, we want to catch the thrown error and then send a custom response to the client
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js (Pages Dir)
- Next.js (App Dir)
- NestJS
import express from "express";
let app = express();
//...
// in your app's error handler, we catch the custom error
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err.message === "Session already exists on another device") {
// TODO: send a custom response using res
return;
}
res.send(500).send(err.message)
})
import Hapi from "hapi";
let server = new Hapi.Server({ port: 8000 });
// first we create a plugin to handle all errors from the app
const plugin = {
name: "...",
version: "...",
register: async function (server: Hapi.Server) {
server.ext("onPreResponse", async (request, h) => {
if ("isBoom" in request.response) {
let err = request.response.data;
if (err.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
}
return h.continue;
});
},
};
// then we register this plugin
(async () => {
await server.register(plugin);
await server.start();
})();
import Fastify from "fastify";
let fastify = Fastify();
fastify.setErrorHandler(async (err: any, req, res) => {
if (err.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
// TODO: send a 500 error with the err.message
});
import middy from "@middy/core";
import cors from "@middy/http-cors";
import SuperTokens from "supertokens-node";
// this is in the auth.js file
import { middleware } from "supertokens-node/framework/awsLambda";
import { getBackendConfig } from "./config";
module.exports.handler = middy(middleware()).use(cors({
origin: getBackendConfig().appInfo.websiteDomain,
credentials: true,
headers: ["Content-Type", ...SuperTokens.getAllCORSHeaders()].join(", "),
methods: "OPTIONS,POST,GET,PUT,DELETE"
})).onError(request => {
if (request.error !== null && request.error.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
throw request.error;
});
import Koa from "koa";
import { middleware } from "supertokens-node/framework/koa";
let app = new Koa();
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: return a custom response
}
throw err;
}
})
app.use(middleware());
import { Next } from "@loopback/core";
import { RestApplication, Middleware, MiddlewareContext } from "@loopback/rest";
import { middleware } from "supertokens-node/framework/loopback";
let app = new RestApplication();
export const customErrorMiddleware: Middleware = async (ctx: MiddlewareContext, next: Next) => {
try {
return await next();
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: return a custom response
}
throw err;
}
};
app.middleware(middleware);
app.middleware(customErrorMiddleware);
import { superTokensNextWrapper } from "supertokens-node/nextjs";
import { middleware } from "supertokens-node/framework/express";
// in the /auth/[[...path]].tsx file
export default async function superTokens(req: any, res: any) {
//...
try {
await superTokensNextWrapper(
async (next) => {
// Refer to the NextJS integration guide to know why this is needed
res.setHeader(
"Cache-Control",
"no-cache, no-store, max-age=0, must-revalidate"
);
await middleware()(req, res, next)
},
req,
res
)
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: send custom reply
}
throw err;
}
//...
}
import { getAppDirRequestHandler } from "supertokens-node/nextjs";
import { NextRequest, NextResponse } from "next/server";
import SuperTokens from "supertokens-node";
import { backendConfig } from "@/app/config/backend";
SuperTokens.init(backendConfig());
// in the app/api/auth/[...path]/route.ts file
const handleCall = getAppDirRequestHandler();
const withCustomErrorHandling = async (request: NextRequest) => {
try {
return await handleCall(request);
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: send custom reply
}
throw err;
}
};
export async function GET(request: NextRequest) {
const res = await withCustomErrorHandling(request);
if (!res.headers.has("Cache-Control")) {
// Refer to the NextJS integration guide to know why this is needed
res.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
}
return res;
}
export const POST = withCustomErrorHandling;
export const DELETE = withCustomErrorHandling;
export const PUT = withCustomErrorHandling;
export const PATCH = withCustomErrorHandling;
export const HEAD = withCustomErrorHandling;
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { errorHandler } from 'supertokens-node/framework/express';
import { Error as STError } from 'supertokens-node';
// we want to add our own error handler which will catch the special exception
@Catch(STError)
export class AppErrorHandler implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
if (exception.message === "Session already exists on another device") {
// TODO: send custom error using ctx.getResponse<Response>()
} else {
throw exception;
}
}
}
import (
"net/http"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
OnSuperTokensAPIError: func(err error, req *http.Request, res http.ResponseWriter) {
if err.Error() == "Session already exists on another device" {
// TODO: send custom error
}
// TODO: send generic error
},
})
}
- FastAPI
- Flask
- Django
from fastapi import FastAPI
app = FastAPI()
@app.exception_handler(Exception)
async def exception_handler(_, exc: Exception):
if str(exc) == "Session already exists on another device":
pass # TODO: send custom response
# TODO: Send generic 500 response
from flask import Flask
app = Flask(__name__)
@app.errorhandler(Exception)
def all_exception_handler(exc: Exception):
if str(exc) == "Session already exists on another device":
pass # TODO: send custom response
# TODO: Send generic 500 response
# Add this middlware in settings.py
from django.http import HttpRequest, HttpResponse
class ErrorHandlerMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
response: HttpResponse = self.get_response(request)
return response
def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse:
if exception and str(exception) == "Session already exists on another device":
pass # TODO: send custom response
return HttpResponse("Error processing the request.", status=500)