Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js 14 Ensure you return a Response or a NextResponse in all branches of your handler. #240

Open
mkbctrl opened this issue Dec 26, 2023 · 5 comments

Comments

@mkbctrl
Copy link

mkbctrl commented Dec 26, 2023

Here is the route handler implementation:

const router = createEdgeRouter<NextRequest, AuthCallbackReqContext>()

router.use(handleAuthServerError, handleAuthMissingCodeError).get((req) => {
  const { origin } = new URL(req.url)
  return NextResponse.redirect(origin)
})

export async function GET(request: NextRequest, ctx: AuthCallbackReqContext) {
  return router.run(request, ctx)
}

As you can see it's pretty simple GET endpoint. For simplicity, I removed the GET related logic, as it seems it doesn't have anything to do with the problem.

Currently, when I am redirected from my auth form (Supabase Auth) to the http://localhost:3000/api/v1/auth/callback?code=some-random-values I get the following error:

 ⨯ Error: No response is returned from route handler '.../api/v1/auth/callback/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.
    at .../node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:6:63416
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

Which is odd, as I consider return NextResponse.redirect(origin) a valid response 🤔

Here is the request object to provide more context:

############ ROUTER GET NextRequest [Request] {
  [Symbol(realm)]: {
    settingsObject: { baseUrl: undefined, origin: [Getter], policyContainer: [Object] }
  },
  [Symbol(state)]: {
    method: 'GET',
    localURLsOnly: false,
    unsafeRequest: false,
    body: null,
    client: { baseUrl: undefined, origin: [Getter], policyContainer: [Object] },
    reservedClient: null,
    replacesClientId: '',
    window: 'client',
    keepalive: false,
    serviceWorkers: 'all',
    initiator: '',
    destination: '',
    priority: null,
    origin: 'client',
    policyContainer: 'client',
    referrer: 'client',
    referrerPolicy: '',
    mode: 'cors',
    useCORSPreflightFlag: false,
    credentials: 'same-origin',
    useCredentials: false,
    cache: 'default',
    redirect: 'follow',
    integrity: '',
    cryptoGraphicsNonceMetadata: '',
    parserMetadata: '',
    reloadNavigation: false,
    historyNavigation: false,
    userActivation: false,
    taintedOrigin: false,
    redirectCount: 0,
    responseTainting: 'basic',
    preventNoCacheCacheControlHeaderModification: false,
    done: false,
    timingAllowFailed: false,
    headersList: _HeadersList {
      cookies: null,
      [Symbol(headers map)]: [Map],
      [Symbol(headers map sorted)]: [Array]
    },
    urlList: [ URL {} ],
    url: URL {
      href: 'http://localhost:3000/api/v1/auth/callback?code=some-code',
      origin: 'http://localhost:3000',
      protocol: 'http:',
      username: '',
      password: '',
      host: 'localhost:3000',
      hostname: 'localhost',
      port: '3000',
      pathname: '/api/v1/auth/callback',
      search: '?code=some-code',
      searchParams: URLSearchParams { 'code' => 'some-code' },
      hash: ''
    }
  },
  [Symbol(signal)]: AbortSignal { aborted: false },
  [Symbol(abortController)]: AbortController { signal: AbortSignal { aborted: false } },
  [Symbol(headers)]: _HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(20) {
      'accept' => [Object],
      'accept-encoding' => [Object],
      'accept-language' => [Object],
      'connection' => [Object],
      'cookie' => [Object],
      'host' => [Object],
      'referer' => [Object],
      'sec-ch-ua' => [Object],
      'sec-ch-ua-mobile' => [Object],
      'sec-ch-ua-platform' => [Object],
      'sec-fetch-dest' => [Object],
      'sec-fetch-mode' => [Object],
      'sec-fetch-site' => [Object],
      'sec-fetch-user' => [Object],
      'upgrade-insecure-requests' => [Object],
      'user-agent' => [Object],
      'x-forwarded-for' => [Object],
      'x-forwarded-host' => [Object],
      'x-forwarded-port' => [Object],
      'x-forwarded-proto' => [Object]
    },
    [Symbol(headers map sorted)]: [
      [Array], [Array], [Array],
      [Array], [Array], [Array],
      [Array], [Array], [Array],
      [Array], [Array], [Array],
      [Array], [Array], [Array],
      [Array], [Array], [Array],
      [Array], [Array]
    ]
  },
  [Symbol(internal request)]: {
    cookies: RequestCookies { _parsed: [Map], _headers: [_HeadersList] },
    geo: {},
    ip: undefined,
    nextUrl: NextURL { [Symbol(NextURLInternal)]: [Object] },
    url: 'http://localhost:3000/api/v1/auth/callback?code=some-code'
  }
} { params: undefined }

Middlewares are pretty straightforward as well:

export const handleAuthServerError = async (
  req: NextRequest,
  _: AuthCallbackReqContext,
  next: NextHandler,
) => {
  const redirectUrl = hasAuthServerError(new URL(req.url))

  if (redirectUrl) {
    return NextResponse.redirect(redirectUrl)
  }

  await next()
}

export const handleAuthMissingCodeError = async (
  req: NextRequest,
  _: AuthCallbackReqContext,
  next: NextHandler,
) => {
  const redirectUrl = await hasAuthVerifyCode(new URL(req.url))

  if (redirectUrl) {
    return NextResponse.redirect(redirectUrl)
  }

  await next()
}

It's my attempt to convert my route to next-connect structure, it seems to me it's some sort of an issue with next-connect. However I am not next-connect heavy user, so messing smth up is also an option. It is however my 2nd issue with next-connect while working with the newest next.js 14. While working with older versions I didn't have issues like that.

Here is my regular route, that works without any issues:

export async function GET(req: NextRequest) {
  const url = new URL(req.url)

  try {
    const redirectAuthErrorUrl = hasAuthServerError(url) ?? (await hasAuthVerifyCode(url))

    if (redirectAuthErrorUrl) {
      throw new AuthErrorWithRedirect('Authentication middleware failure.', redirectAuthErrorUrl)
    }

    const { searchParams, origin } = url
    const code = searchParams.get('code')! // hasAuthVerifyCode ensures the code is present
    const { dbServerClient } = composeDbServerClient({
      cookieMethods: composeCookieMethods(),
    })

    const response = await dbServerClient.auth.exchangeCodeForSession(code)

    if (response.error) {
      throw response.error
    }

    return NextResponse.redirect(origin)
  } catch (error) {
    logger.error(`Callback authentication error occurred. ${JSON.stringify(error)}`)

    ...error handling logic
}

@mkbctrl
Copy link
Author

mkbctrl commented Dec 28, 2023

We have a suspect, that next-intl can potentially alter responses in a way the produces this error. For now it's just an unconfirmed assumption, but next-intl is what we added to the stack that was working smoothly earlier on.

We also run into another issue with next-intl, maybe someone with more context will be able to connect those dots faster:
amannn/next-intl#728

@albertpeiro
Copy link

albertpeiro commented Jan 7, 2024

I'm having this issue too.
Some findings:
This works:

api.use((req, event, next) => {
  return next(); // call next in chain
});

This does Not work:

api.use(async (req, event, next) => {
  await next(); // call next in chain
});

Yielding error:

 ⨯ Error: No response is returned from route handler '......../src/app/api/waitlist/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.

▲ Next.js 14.0.4

@bigWhiteWhite
Copy link

This is how I used it before, and I also got this error.

const router = createEdgeRouter<NextRequest, RequestContext>()

router
	// A middleware example
	.use(async (request, event, next) => {
		const start = Date.now()
		await next() // call next in chain
		const end = Date.now()
		console.log(`Request took ${end - start}ms`)
	})
	.get(async (request) => {
		try {
			return NextResponse.json([])
		} catch (error: any) {
			console.log('🚀 ~ GET ~ error:', error)
			return NextResponse.json([])
		}
	})
export async function GET(request: NextRequest, ctx: RequestContext) {
	return router.run(request, ctx)
}

Later I changed it to

const router = createEdgeRouter<NextRequest, RequestContext>()

router
	// changed here
	.use((request, event, next) => {
		const start = Date.now()
		const end = Date.now()
		console.log(`Request took ${end - start}ms`)
	    return next()
	})
	.get(async (request) => {
		try {
			return NextResponse.json([])
		} catch (error: any) {
			console.log('🚀 ~ GET ~ error:', error)
			return NextResponse.json([])
		}
	})
export async function GET(request: NextRequest, ctx: RequestContext) {
	return router.run(request, ctx)
}

Then it's ok

@Yukititit
Copy link

router
	.use(async (request, event, next) => {
		const start = Date.now()
		const end = Date.now()
		console.log(`Request took ${end - start}ms`)
	        return (await next())
	})
	.get(async (request) => {
		return Response.json([])
	})
}

Also works for me in Nextjs 14.1 while keeping the the first function async.

@daveteu
Copy link

daveteu commented Apr 24, 2024

Since we need to return next(), I moved the timer to .finally() since I haven't found a way to have it in the middleware.

router
	.use(async (request, event, next) => {
	        return (await next())
	})
	.get(async (request) => {
		return Response.json([])
	})
}

export const GET = (request: NextRequest, ctx: RequestContext) => {

  const start = Date.now();
  return router.run(request, ctx).finally(() => {
    const end = Date.now();
    console.log(`Request took ${end - start}ms`);

  });
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants