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

Edge Functions silently falling back to Nodejs Runtime #38743

Closed
1 task done
Saeris opened this issue Jul 17, 2022 · 5 comments · Fixed by #38750
Closed
1 task done

Edge Functions silently falling back to Nodejs Runtime #38743

Saeris opened this issue Jul 17, 2022 · 5 comments · Fixed by #38750
Labels
kind: bug Confirmed bug that is on the backlog Runtime Related to Node.js or Edge Runtime with Next.js.

Comments

@Saeris
Copy link

Saeris commented Jul 17, 2022

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

PS C:\Users\drake\GitHub\@saeris\edge-fail> yarn next info

    Operating System:        
      Platform: win32        
      Arch: x64
      Version: Windows 10 Pro
    Binaries:
      Node: 18.4.0
      npm: N/A
      Yarn: N/A
      pnpm: 7.4.0
    Relevant packages:
      next: 12.2.3-canary.10
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0

warn  - Latest canary version not detected, detected: "12.2.3-canary.10", newest: "12.2.3-canary.9".
        Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.
        Read more - https://nextjs.org/docs/messages/opening-an-issue

What browser are you using? (if relevant)

N/A

How are you deploying your application? (if relevant)

Vercel

Describe the Bug

I first noticed this behavior when upgrading a Nextjs 12.1 project to 12.2, having followed the migration guide to convert my Nodejs API routes to the new Edge function runtime. Quickly I found out that my routes weren't behaving as expected, as I was getting an error from the following line:

const { searchParams, origin } = new URL(req.url);

Here I am following the recommendation from the middleware migration guide here (Please note, it has been mentioned in the docs that the Edge Runtime and Middleware share the same function signature and runtime API). This pattern is also mentioned in the official Edge Functions docs. This results in a runtime error because req.url is not a complete url, it's only the pathname.

error - TypeError [ERR_INVALID_URL]: Invalid URL
    at new NodeError (node:internal/errors:388:5)
    at URL.onParseError (node:internal/url:564:9)
    at new URL (node:internal/url:644:5)
    at handler (webpack-internal:///(api)/./src/pages/api/check.ts:27:40)
    at Object.apiResolver (C:\Users\drake\GitHub\@saeris\worbik\node_modules\next\dist\server\api-utils\node.js:179:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async DevServer.runApi (C:\Users\drake\GitHub\@saeris\worbik\node_modules\next\dist\server\next-server.js:381:9)
    at async Object.fn (C:\Users\drake\GitHub\@saeris\worbik\node_modules\next\dist\server\base-server.js:497:37)
    at async Router.execute (C:\Users\drake\GitHub\@saeris\worbik\node_modules\next\dist\server\router.js:213:36)
    at async DevServer.run (C:\Users\drake\GitHub\@saeris\worbik\node_modules\next\dist\server\base-server.js:616:29) {
  input: '/api/check?guess=5inrvareifaodavmlLbaolklbA&challenge=5inrvaeifarodavmlLbaolklbA199',
  code: 'ERR_INVALID_URL',
  page: '/api/check'
}

This lead me to investigate further and so I put together a minimal edge route to log out the NextRequest and NextFetchEvent arguments to see what I was getting back from them:

import type { NextMiddleware } from "next/server";

export const config = {
  runtime: `experimental-edge`,
};

const handler: NextMiddleware = (req, event) => {
  // req is a NextRequest as the types indicate
  // event should be a NextFetchEvent, instead it's a ServerResponse
  // eslint-disable-next-line no-console
  console.log({ req, event, url: req.url });
  return new Response();
};

export default handler;

In the minimal reproduction I've provided, running yarn dev and then visiting localhost:3000/api/edge will lead to the following to be logged in your terminal:

PS C:\Users\drake\GitHub\@saeris\edge-fail> yarn dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
warn  - You have enabled experimental feature (images) in next.config.js.
warn  - Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

event - compiled client and server successfully in 296 ms (153 modules)
wait  - compiling /api/edge...
event - compiled successfully in 20 ms (31 modules)
{
  req: IncomingMessage {
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: true,
      ended: true,
      endEmitted: true,
      reading: false,
      constructed: true,
      sync: true,
      needReadable: false,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: false,
      emitClose: true,
      autoDestroy: true,
      destroyed: true,
      errored: null,
      closed: true,
      closeEmitted: true,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: true,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      parser: [HTTPParser],
      on: [Function: socketListenerWrap],
      addListener: [Function: socketListenerWrap],
      prependListener: [Function: socketListenerWrap],
      setEncoding: [Function: socketSetEncoding],
      _paused: false,
      _httpMessage: [ServerResponse],
      [Symbol(async_id_symbol)]: 2744,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(kSetKeepAlive)]: false,
      [Symbol(kSetKeepAliveInitialDelay)]: 0,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    httpVersionMajor: 1,
    httpVersionMinor: 1,
    httpVersion: '1.1',
    complete: true,
    rawHeaders: [
      'Host',
      'localhost:3000',
      'Connection',
      'keep-alive',
      'Cache-Control',
      'max-age=0',
      'sec-ch-ua',
      '" Not A;Brand";v="99", "Chromium";v="102", "Microsoft Edge";v="102"',
      'sec-ch-ua-mobile',
      '?0',
      'sec-ch-ua-platform',
      '"Windows"',
      'DNT',
      '1',
      'Upgrade-Insecure-Requests',
      '1',
      'User-Agent',
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44',     
      'Accept',
      'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
      'Sec-Fetch-Site',
      'none',
      'Sec-Fetch-Mode',
      'navigate',
      'Sec-Fetch-User',
      '?1',
      'Sec-Fetch-Dest',
      'document',
      'Accept-Encoding',
      'gzip, deflate, br',
      'Accept-Language',
      'en-US,en;q=0.9'
    ],
    rawTrailers: [],
    aborted: false,
    upgrade: false,
    url: '/api/edge',
    method: 'GET',
    statusCode: null,
    statusMessage: null,
    client: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      parser: [HTTPParser],
      on: [Function: socketListenerWrap],
      addListener: [Function: socketListenerWrap],
      prependListener: [Function: socketListenerWrap],
      setEncoding: [Function: socketSetEncoding],
      _paused: false,
      _httpMessage: [ServerResponse],
      [Symbol(async_id_symbol)]: 2744,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(kSetKeepAlive)]: false,
      [Symbol(kSetKeepAliveInitialDelay)]: 0,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _consuming: false,
    _dumped: false,
    cookies: [Getter/Setter],
    query: {},
    previewData: [Getter/Setter],
    preview: [Getter/Setter],
    body: '',
    [Symbol(kCapture)]: false,
    [Symbol(kHeaders)]: {
      host: 'localhost:3000',
      connection: 'keep-alive',
      'cache-control': 'max-age=0',
      'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="102", "Microsoft Edge";v="102"',
      'sec-ch-ua-mobile': '?0',
      'sec-ch-ua-platform': '"Windows"',
      dnt: '1',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44',
      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',       
      'sec-fetch-site': 'none',
      'sec-fetch-mode': 'navigate',
      'sec-fetch-user': '?1',
      'sec-fetch-dest': 'document',
      'accept-encoding': 'gzip, deflate, br',
      'accept-language': 'en-US,en;q=0.9'
    },
    [Symbol(kHeadersCount)]: 32,
    [Symbol(kTrailers)]: null,
    [Symbol(kTrailersCount)]: 0,
    [Symbol(NextRequestMeta)]: {
      __NEXT_INIT_URL: 'http://localhost:3000/api/edge',
      __NEXT_INIT_QUERY: {},
      _protocol: 'http',
      __nextHadTrailingSlash: false,
      __nextIsLocaleDomain: false
    }
  },
  event: <ref *1> ServerResponse {
    _events: [Object: null prototype] {
      finish: [Function: bound resOnFinish],
      pipe: [Function]
    },
    _eventsCount: 2,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    _closed: false,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      parser: [HTTPParser],
      on: [Function: socketListenerWrap],
      addListener: [Function: socketListenerWrap],
      prependListener: [Function: socketListenerWrap],
      setEncoding: [Function: socketSetEncoding],
      _paused: false,
      _httpMessage: [Circular *1],
      [Symbol(async_id_symbol)]: 2744,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(kSetKeepAlive)]: false,
      [Symbol(kSetKeepAliveInitialDelay)]: 0,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _header: null,
    _keepAliveTimeout: 5000,
    _onPendingData: [Function: bound updateOutgoingData],
    req: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      socket: [Socket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      rawHeaders: [Array],
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '/api/edge',
      method: 'GET',
      statusCode: null,
      statusMessage: null,
      client: [Socket],
      _consuming: false,
      _dumped: false,
      cookies: [Getter/Setter],
      query: {},
      previewData: [Getter/Setter],
      preview: [Getter/Setter],
      body: '',
      [Symbol(kCapture)]: false,
      [Symbol(kHeaders)]: [Object],
      [Symbol(kHeadersCount)]: 32,
      [Symbol(kTrailers)]: null,
      [Symbol(kTrailersCount)]: 0,
      [Symbol(NextRequestMeta)]: [Object]
    },
    _sent100: false,
    _expect_continue: false,
    statusCode: 200,
    flush: [Function: flush],
    write: [Function (anonymous)],
    end: [Function (anonymous)],
    on: [Function: on],
    writeHead: [Function: writeHead],
    status: [Function (anonymous)],
    send: [Function (anonymous)],
    json: [Function (anonymous)],
    redirect: [Function (anonymous)],
    setPreviewData: [Function (anonymous)],
    clearPreviewData: [Function (anonymous)],
    revalidate: [Function (anonymous)],
    unstable_revalidate: [Function (anonymous)],
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: null,
    [Symbol(kUniqueHeaders)]: null
  },
  url: '/api/edge'
}

As you can see, clearly we're not getting the expected data types here. The above matches what you would expect from the standard Nodejs runtime, where NextRequest extends IncomingMessage and NextResponse extends ServerResponse. Additionally as you can see, req.url was logged as /api/edge, which explains why the code I wrote earlier to extract request parameters resulted in a runtime error.

From here the next steps I tried were to run yarn why next to determine if I hadn't actually upgraded to 12.2. Nothing appeared to be wrong there. So to be safe, I nuked my node_modules, yarn lockfile, etc and reinstalled. Same issue. Another developer suggested I try using another 12.2 feature to verify that I was indeed running it, so I enabled next/future/image in my next.config.js, which you can see from the console output above I'm properly getting warned about on startup.

It was from this point I started putting together a minimal, because as far as I can tell I'm doing all I need to in order to opt into using the Edge runtime. With that together, I was able to reproduce this failure on two additional machines (Windows 10 and macOS Monterey) as well as inside of GitHub Codespaces and asking another engineer to confirm.

Expected Behavior

I just want to be able to use the Edge runtime for my API routes and have a clear understanding of why the documented configuration instructions aren't working. 😭

Link to reproduction

https://github.com/Saeris/edge-fail

To Reproduce

Please refer to the linked repository README for additional details

Steps to Reproduce

  • Install deps: yarn
  • Check Nextjs version: yarn why next
  • Start dev server: yarn dev
  • Review console output, should have warning for experimental Nextjs feature (images), verifying the server is ^v12.2.0. Missing is a warning about experimental edge runtime.
  • Visit api route: http://localhost:3000/api/edge
  • Review console output, should be following shape:
{
    req: IncomingMessage {
        ...
    },
    event: ServerResponse {
        ...
    },
    url: '/api/edge'
}
@Saeris Saeris added the bug Issue was opened via the bug report template. label Jul 17, 2022
@SukkaW
Copy link
Contributor

SukkaW commented Jul 18, 2022

I am able to reproduce the bug. I suspect it is caused by Next.js' built-in static analysis (The runtime config is determined at the build time).

In the meantime, you can work around it by changing the quote:

export const config = {
-  runtime: `experimental-edge`
+  // change from ` to '
+  runtime: 'experimental-edge'
}

Update

function extractValue(node: Node): any {

I have located the issue. It seems that Next.js is not able to statically extract value from TemplateLiteral. I am now implementing a fix, and once the test case passed I will submit a PR.

Update

I have submitted a PR.

SukkaW added a commit to SukkaW/next.js that referenced this issue Jul 18, 2022
SukkaW added a commit to SukkaW/next.js that referenced this issue Jul 18, 2022
@balazsorban44 balazsorban44 added kind: bug Confirmed bug that is on the backlog Runtime Related to Node.js or Edge Runtime with Next.js. and removed bug Issue was opened via the bug report template. labels Jul 18, 2022
@Saeris
Copy link
Author

Saeris commented Jul 18, 2022

Incredible. I've been using backticks for years and this is the first time I've ever encountered a bug because of them. Explains why I was having such a hard time putting together a reproduction too. My minimal isn't using the eslint ruleset I typically use in my projects, so it wasn't auto-fixing the config string to use backticks. Explains why when I copied the code from the original broken project over to the minimal that it broke.

Thanks for implementing a fix! Again I can't believe it came down to something as mundane as quotes.

@SukkaW
Copy link
Contributor

SukkaW commented Jul 18, 2022

@Saeris

In Next.js, config.runtime is not runtime evaluated, as it is used to determine which runtime your code will be executed in. So it has to be statically analyzed at build time (before execution).
And 'experimental-edge' and `experimental-edge` are different in JavaScript. The first one is a StringLiteral while the second one is a TemplateLiteral.

Although unable to handle TemplateLiteral during static analysis is indeed a bug of Next.js, IMHO you should avoid using backtick quotes in your code where single/double quotes are possible. TemplateLiteral has an extra performance overhead compared to StringLiteral and would potentially impact the performance when accumulated.

@Saeris
Copy link
Author

Saeris commented Jul 18, 2022

TemplateLiteral has an extra performance overhead compared to StringLiteral and would potentially impact the performance when accumulated.

I'd need to see some convincing perf data to change my mind about this one. Everything I've read up to now suggests either the opposite is true or that the difference is so small (<1%) between browser environments that it doesn't matter. The perf cost adds up with the more interpolations there are in your string, so in a case such as this, where there are no interpolations, you'd never see that overhead accrue.

Perf concerns aside, while I may be an outlier here in terms of my preference for backticks, it doesn't seem unreasonable to me that others could encounter the same problem and be caught unaware of the internal behavior Nextjs has for evaluating config settings. I'm fully aware that they are different data types, but it's difficult to know exactly when that distinction is going to make a difference, as it so rarely does in this case. From the user perspective, if it's valid JS then it should just work. At best, it could be inferred that an exported config object will be evaluated as JSON and if it's not serializable as such, then it could have unexpected behavior.

Of course, documentation would clear up this ambiguity. Might be worth considering, if it does not exist already, adding a NextAPIConfig type that mentions this as part of its JSDoc comment. I'd imagine it would rarely get used but that would be a good place to put it. Same goes for any other configs that share this static analysis behavior.

@kodiakhq kodiakhq bot closed this as completed in #38750 Jul 21, 2022
kodiakhq bot pushed a commit that referenced this issue Jul 21, 2022
## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

Fixes #38743.
Fixes: #38750

The PR adds basic `TemplateLiteral` support for static analysis.

The corresponding re-production of #38743 has also been implemented in e2e tests.
@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
kind: bug Confirmed bug that is on the backlog Runtime Related to Node.js or Edge Runtime with Next.js.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants