hoangvvo / next-connect

The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2

Home Page:https://www.npmjs.com/package/next-connect

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

mkbctrl opened this issue · comments

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
}

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

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

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

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.

commented

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`);

  });
};