Return 404 page from functions

Is there a canonical way to return the standard 404 page (/404) from a serverless function. I need to make the function try to handle a request, and return the 404 page if it can’t.

export const handler: Handler = async (event, context) => {
   const result = trytohandle(event);
   if (result) {
     // Construct a response.
   }
  // Fall back to standard 404
};

Looking at the how to create a function documentation, the hello.ts example is…

const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Hello World" }),
  };
};

So applying what you want to that…

const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
  const result = trytohandle(event);
  if (result) {
    // Construct a response.
  }
  return {
    statusCode: 404,
    body: JSON.stringify({ message: "Not Found" }),
  };
};

I’ve just noticed you literally want “the 404 page”, returned as HTML content, not just a “404 status”.

To do that you could either embed the content for it into the function and return it as the body of the request, ensuring only a 404 is returned.

Or you could 302 to the /404 page, with something along the lines of:

return {
  statusCode: 302,
  headers: {
    'Location': '/404'
  }
};

Thanks for the response!
Yes, it should return the actual content of /404.html since that is managed by editors in a CMS.
A redirect status code is not an option because the url must not change, and i think it also confuses search engines.

Context: I’m trying to layer a Netlify website over an existing legacy system, gradually replacing the latter. Pages in Netlify should have precedence, and for everything else, the legacy system should get its chance.

I’m currently experimenting with a creative mix of rewrites and edge functions:

A rewrite at the very end that catches everything Netlify does not know and passes it to the edge handler, and - before that - one that breaks an infinite rewrite loop and returns 404.

/_legacy/* /404 404
/* https://myapp.netlify.app/_legacy/:splat 200

And the edge function that passes the request to the legacy system (pseudo-code):

export default async function legacy(request) {
  const result = await passToLegacySystem(request);
  if (result.status !== 404) {
    // Return the legacy system response.
    return result;
  } 
  // Abort the edge function to let rewrites handle the request.
  return;
}
export const config = { path: '/_legacy/*' };

This seems to work. My only gripe is the rewrite back to the edge function that has to re-initiate the whole request and causes infinite loops when something is just slightly off. But i suppose there is no other way, since we can’t rewrite to an edge function directly.

Even though your 404 page is CMS controlled, since the ultimate HTML content is just string, you could easily retrieve it after it’s produced by your build process and ensure that it’s literally within, (or accessible by), the function code that is deployed.

I think thats the part I’m looking for. And your comment sparked the right search terms :grin:

I think this is ultimately what i need: How to Include Files in Netlify Serverless Functions

Thank you!

Yep, that’s what I meant by “accessible by”.

When I said “ensure it’s literally within”, I mean an approach where you read the 404 file contents only once after your main build, but before deployment, and then do simple string token replacement to write it into the function code prior to it being deployed. Which negates the need for the file to be included in the bundle and read on each and every request.