vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.85k stars 26.86k forks source link

Way to return 404 http error code in render() #4452

Closed zsolt-dev closed 5 years ago

zsolt-dev commented 6 years ago

Is your feature request related to a problem? Please describe. Right now you can return 404 http error code only in getInitialProps, which is not nice, since you have to fetch the data twice. Once in getInitialProps to return the right http response code and once in the component itself when using apollo, so you will have loading property etc.

Describe the solution you'd like With react-router-dom you can have a component like this:

import React from 'react';
import { Route } from 'react-router-dom';

const NotFound = () => {
  return (
    <Route render={({ staticContext }) => {
      if (staticContext) {
        staticContext.status = 404;
      }
      return (
        <div>
          <h1>404 : Not Found</h1>
        </div>
      )
    }}/>
  );
};

export default NotFound;

With this component, you can simply have your apollo HOC near the components and SSR returns the correct http codes without the need for getInitialProps.

It would be great to be able to return http codes from render()

shauns commented 6 years ago

Similar use-case -- I use Apollo for GraphQL and I would like to return a 404 on server renders where the result of a query is (for instance) empty.

My <Query> lives inside render as expected (in fact, same as the example Next.JS+Apollo code). So in getInitialProps I don't have access to the [empty] data yet in order to return a 404 through res.

My current workaround is pretty gross:


<Query query={getResponses} variables={{ foo: this.props.bar }}>
  {({ loading, error, data }) => {
    // Check for error, loading etc.
    const responses = data.allResponses.nodes;
    if (responses.length === 0) {
      const e = new Error("Response not found");
      e.code = "ENOENT";  // Triggers a 404
      throw e;
    }
timneutkens commented 6 years ago

Nearly the same issue: #4451 (just keeping track)

julkue commented 6 years ago

@timneutkens Yes it is. But, I also have the GraphQl use case of @shauns and in my case I can differentiate between a not found page and a invalid page (for example using a page type that is not supported in the frontend). Therefore I'd like to also differentiate in the resulting status code:

I can do this with the next/error component but that will not result in the expected HTTP status code. And with the solution of @shauns there's no way to differentiate between 404 and 400. What do you suggest?

kachkaev commented 6 years ago

@shauns thanks for sharing this trick with e.code = "ENOENT" – it works for me when the page is rendered on the server. How would you achieve the same behaviour with client-side rendering? E.g. when a person clicks on a product in a list, but it has just been deleted. At the moment, I see this instead of my usual 404 page:

screen shot 2018-06-02 at 11 09 00

Refreshing the page shows a proper 404 error page, but that's because Apollo fetching is done on the server this time.

adamsoffer commented 5 years ago

@kachkaev you ever figure out how to achieve the same behavior with client-side rendering?

adamsoffer commented 5 years ago

@kachkaev here's a workaround...

import Error from 'next/error'

export const throw404 = () => {
  if (process.browser) {
    return <Error statusCode={404} />
  }
  const e = new Error()
  e.code = 'ENOENT'
  throw e
}
timneutkens commented 5 years ago

Outlined a solution here: https://spectrum.chat/next-js/general/error-handling-in-async-getinitialprops~99400c6c-0da8-4de5-aecd-2ecf122e8ad0?m=MTUzOTUyMTA1OTE0Mw==

andreyvital commented 5 years ago

I ended up doing something like this (don't know if it's an anti-pattern):

// lib/ServerResponseContext.js
import { createContext } from "react";

export default createContext(null);

// lib/withServerResponse.js
import PropTypes from "prop-types";
import React from "react";
import ServerResponseContext from "./ServerResponseContext";

export default App => {
  return class extends React.Component {
    static displayName = "withServerResponse(App)";

    static propTypes = {
      response: PropTypes.func
    };

    static defaultProps = {
      response: () => null
    };

    static async getInitialProps(context) {
      const { ctx } = context;
      let props = {};

      if (App.getInitialProps) {
        props = await App.getInitialProps(context);
      }

      return {
        ...props,
        response: () => ctx.res
      };
    }

    render() {
      const { response, ...props } = this.props;

      if (process.browser) {
        return <App {...props} />;
      }

      return (
        <ServerResponseContext.Provider value={response()}>
          <App {...props} />
        </ServerResponseContext.Provider>
      );
    }
  };
};

// lib/StatusCode.js
import PropTypes from "prop-types";
import React from "react";
import ServerResponseContext from "./ServerResponseContext";

export default function StatusCode({ statusCode, children }) {
  if (process.browser) {
    return null;
  }

  return (
    <ServerResponseContext.Consumer>
      {response => {
        response.statusCode = statusCode;
        return children;
      }}
    </ServerResponseContext.Consumer>
  );
}

StatusCode.propTypes = {
  statusCode: PropTypes.number.isRequired,
  children: PropTypes.any
};

StatusCode.defaultProps = {
  children: null
};

This way I can figure out the statusCode in render time. It doesn't require me to prefetch all my data in getInitialProps to decide the statusCode (I'm using Apollo as well):

<Query
  query={gql`
    ...
  `}
>
  {({ loading, data }) => {
    if (loading) {
      return <div>Loading...</div>;
    } else if (!data.user) {
      return (
        <>
          <StatusCode statusCode={404} />
          <div>Not found...🤔</div>
        </>
      );
    }

    return (
      <div>...</div>
    )
  }}
</Query>

Let me know what you guys think...

martpie commented 5 years ago

For the lazy ones of you, I wrote a withError HoC a week ago: https://www.npmjs.com/package/next-with-error

Feedback is appreciated :)

andreyvital commented 5 years ago

@martpie I guess one of the "problems" is about returning an error deep down the in the render tree. You only have access to getInitialProps on root components, and in use cases such as Apollo's w/ GraphQL you won't fetch all your queries manually in getInitialProps.

Imagine something like this:

export default function MyPage() {
  return (
    <Query>
      {({ data, loading }) => (
        <div>
          ...
          <Query>
            {({ data, loading }) => {
              if (data.isntThere) {
                return (
                  <div>
                    <Status code={404} /> 
                    Couldn't find what you you were looking for.
                  </div>
                )

              }

              return (
                <div>
                  ...
                </div>
              );
            }}
          </Query>
        </div>
      )}
    </Query>
  )
}

So...I think your solution works only for more generic cases when you have control of all the fetching—am I wrong? 🤔

martpie commented 5 years ago

@andreyvital Yes exactly. It is more for classic REST calls.

Apollo is a bit special because the call is executed in a mix of getInitialProps and render(cf react-apollo implementation). I suggested to my team we don't use the Query component but await apollo.query() in getInitialProps (which is fine for us because then we don't have to care about loading states etc).

We are still discussing this issue, and I have no proper solution for the classic <Query> component yet. I will post something if I find a better way :)

pristas-peter commented 5 years ago

@andreyvital Doing the same, I have also implemented the <Redirect /> component this way.

craigglennie commented 4 years ago

@andreyvital I'm missing something about the withServerResponse HOC: What exactly should I wrap with that function? I don't have a custom _app.js file... but the HOC takes App as a parameter - maybe that's what I'm missing. I assume the HOC is not supposed to wrap individual pages?

Edit: To answer my own question, it does appear that you need a custom _app.js so that you have an App to wrap with the withServerResponse HOC.

dsds191919 commented 4 years ago

@kachkaev here's a workaround...

import Error from 'next/error'

export const throw404 = () => {
  if (process.browser) {
    return <Error statusCode={404} />
  }
  const e = new Error()
  e.code = 'ENOENT'
  throw e
}

How Do you implement e.code = 'ENOENT' on TypeScript When I implement it with TypeScript then I got the error like this

"Property 'code' does not exist on type 'Error<{ statusCode: 12; }>' "

here's code(TypeScript)

 const e = new Error({statusCode:12})
  e.code = 'ENOENT'
  throw e

and Is this right way? pages/Index.js

function MainPage({isfetch}){
  if(isfetch){
    return <Home/>
  }
  const e = new Error()
  e.code = 'ENOENT'
  throw e
  return
} 

MainPage.getInitialProps = async ({res, req, err, reduxStore }) => {
return {isfetch:false}
}

I would be happy if you could reply to me

mlabrum commented 4 years ago

@dsds191919 here's something that works

import React, {FunctionComponent} from "react";
import NextError from 'next/error'

interface ErrCodeException extends Error {
    code?: string;
}

const Throw404: FunctionComponent<{}> = () => {
  if (process.browser) {
    return <NextError statusCode={404} />
  }

  const e = new Error() as ErrCodeException
  e.code = 'ENOENT'
  throw e;
}

export default Throw404;
joaogarin commented 4 years ago

This solution stoped working for me with the 9.2.2 upgrade (getting a 500 error when calling new Error(). Anyone noticing the same?

kachkaev commented 4 years ago

Same issue here as for @joaogarin since recently. I used to throw an error on a product page if I failed to find an object by a slug. The trick was in setting e.code = 'ENOENT'. It seems to be not working any more, so I'm getting 500 for missing products as well as a logged error in the server console. Looks like a breaking change 🤔

timneutkens commented 4 years ago

Looks like a breaking change 🤔

Throwing errors that magically turn into 404 is not a Next.js feature.

I've outlined many times how to correctly render 404: https://github.com/zeit/next.js/issues/4452#issuecomment-450642052

jaydenseric commented 4 years ago

I've published next-server-context, a way to access and even modify the server context via a useServerContext() hook:

import { useServerContext } from 'next-server-context'

export default function ErrorMissing() {
  serverContext = useServerContext()
  if (serverContext) serverContext.response.statusCode = 404
  return (
    <section>
      <h1>Error 404</h1>
      <p>Something is missing.</p>
    </section>
  )
}

This, in combination with next-graphql-react (which can SSR GraphQL errors, unlike Apollo) allows you to have proper HTTP status codes for 403, 404, etc.

Examples:

This hook is very useful for creating other hooks, like useGetCookie() which allows you to isomorphically retrieve the cookie within component render:

import { useServerContext } from 'next-server-context'

export const useGetCookie = () => {
  const serverContext = useServerContext()
  return function getCookie() {
    return typeof window === 'undefined'
      ? serverContext
        ? serverContext.request.headers.cookie
        : undefined
      : document.cookie
  }
}

The way useServerContext() is implemented does not cause any data to be serialized on the server for hydration on the client.

Edit (5 Nov 2020): You might need a smarter useGetCookie implementation than the above example, if there is a chance that the cookie is being set in the response. Then you need to merge the request and response cookies.

ricardo-cantu commented 4 years ago

Came across this when upgrading to 9.3. Simple fix because we use a custom _error.tsx

_error.tsx

Error.getInitialProps = async ctx => {

  const { res, err } = ctx

  // ENOENT comes from api.ts 404 on api call
  const statusCode = err?.code === 'ENOENT' ? 404 : res?.statusCode ?? 500`
.....

api.ts

    const response = await fetch(...)
    if (!response.ok) {
      const body = await response.text()
      const err = new Error(
        `${response.status} on ${response.url}: ${response.statusText}: ${body}`
      )
      // Special next.js hack to force a 404 page
      if (response.status === 404) err.code = 'ENOENT'
      throw err
    }
radeno commented 4 years ago

When you throw Error with code set as ENOENT you can re-set HTTP status code back to 404 in getInitialProps right in custom _error.js page

Error.getInitialProps = async ({ err, res }) => {
  if (err?.code === 'ENOENT') {
    res.statusCode = 404;
  }
}
kolserdav commented 3 years ago

I'm resolve this problem that:

pages/[...404].tsx

import { GetServerSidePropsContext } from 'next'

export function getServerSideProps(context: GetServerSidePropsContext) {
  context.res.statusCode = 404;
  return {
    props: {
      someProp: 'some_value'
    }
  };
}

export default function Page404() {
  return (
    <h1>Page 404</h1>
  );
}
balazsorban44 commented 2 years ago

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.