question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Edge Functions silently falling back to Nodejs Runtime

See original GitHub issue

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'
}

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
Saeriscommented, 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.

2reactions
SukkaWcommented, 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

https://github.com/vercel/next.js/blob/56e760a20397c4e1b0f16edac895cc62f78428de/packages/next/build/analysis/extract-const-value.ts#L130

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Edge Functions – Vercel Docs
Vercel's Edge Functions enable you to deliver dynamic, personalized content with the lightweight Edge Runtime. Our Edge Runtime is more performant and ...
Read more >
The Difference Between Node.js 10 LTS and Node.js 12 LTS
Node.js ® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
Read more >
A first look at Bun: is it really 3x faster than Node.js and Deno?
A new JS runtime focused on performance and being all-in-one (runtime, bundler, package manager, transpiler). So think of it like Node.js, plus ...
Read more >
Understand the Azure IoT Edge runtime and its architecture
The IoT Edge runtime is responsible for the following functions on IoT Edge devices: Install and update workloads on the device.
Read more >
Serverless Next.js Component - Serverless Framework: Plugins
Pages that need server side compute to render are hosted on Lambda@Edge. ... [ ] Next.js 12 features Features like middleware, bot-aware ISR...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found