Netlify edge functions context.next()

I find the documentation for context.next() confusing and even more confusing is the behavior. The definition that the docs give are

context.next() invokes the next item in the request chain. The method returns a Promise containing the Response from the origin that your edge function can modify before returning.

I don’t exactly understand 100% what from the origin that your edge function can modify before returning means in the above definition.

Just to experiment I created three files

x.js

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  console.log(data)
}

y.js

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  console.log(data)
}

z.js

export default async (req, context) => {
  console.log('z')
  return Response.json({ msg: 'hi' })
}

In the netlify.toml file I have them all running on the same path

[[edge_functions]]
  path = "/abc"
  function = "x"

[[edge_functions]]
  path = "/abc"
  function = "y"

[[edge_functions]]
  path = "/abc"
  function = "z"

This gives me the following output

[z] z
[y] { msg: "hi" }
[z] z
[x] { msg: "hi" }
[z] z
[y] { msg: "hi" }
[z] z

If someone could clarify why z is logged four times and why the output looks like this that would be great. I understand that the request hits x which invokes y which invokes z. I expect to see z then y then x, but after y finishes z logs to the console again, then the rest I can’t understand.

There might be multiple requests happening locally. I’d advise:

  • checking in production (unless this was already in production)
  • log request ID along with the data to check which log is for which request.

It is running locally, but if I just console.log a string and do not call next it works as expected.

For example

// x.js

export default async (req,context) => {
  console.log('function x')
}

// y.js

export default async (req, context) => {
  console.log('function y')
}

// z.js

export default async (req, context) => {
  console.log('function z')
  return Response.json({ msg: 'hi' })
}

This would output

[x] function x
[y] function y
[z] function z

Did you try logging the request ID as I suggested?

Yes, I did try this. I tried it in production and logged the requestId, both the Netlify edge function logs and locally show the same output.

Based on some testing, I have a different setup going.

/**
 * x.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 * y.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 * z.js
 */

export default async (req, context) => {
  const count = 1
  console.log(count)
  return Response.json({ count })
}

Output

[z] 1
[y] 2
[x] 3

Remove return from y.js

/**
 * x.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 *y.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
 //  return Response.json({ count })
}

/**
 * z.js
 */

export default async (req, context) => {
  const count = 1
  console.log(count)
  return Response.json({ count })
}

Output

[z] 1
[y] 2
[z] 1
[x] 2

Remove return from x.js

/**
 * x.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  // return Response.json({ count })
}


/**
 * y.js
 */

export default async (req, context) => {
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 * z.js
 */

export default async (req, context) => {
  const count = 1
  console.log(count)
  return Response.json({ count })
}

Output

[z] 1
[y] 2
[x] 3
[z] 1
[y] 2

In order to add a bit of context, I have included an additional console.log() at the beginning of each function from the example before.

Removing return from y.js

/**
 * x.js
 */

export default async (req, context) => {
  console.log('x() invoked')
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 * y.js
 */

export default async (req, context) => {
  console.log('y() invoked')
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  // return Response.json({ count })
}

/**
 * z.js
 */

export default async (req, context) => {
  console.log('z() invoked')
  const count = 1
  console.log(count)
  return Response.json({ count })
}

Output

[x] x() invoked
[y] y() invoked
[z] z() invoked
[z] 1
[y] 2
[z] z() invoked
[z] 1
[x] 2

Removing return from x.js

/**
 * x.js
 */

export default async (req, context) => {
  console.log('x() invoked')
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  // return Response.json({ count })
}

/**
 * y.js
 */

export default async (req, context) => {
  console.log('y() invoked')
  const res = await context.next()
  const data = await res.json()
  const count = data.count + 1
  console.log(count)
  return Response.json({ count })
}

/**
 * z.js
 */

export default async (req, context) => {
  console.log('z() invoked')
  const count = 1
  console.log(count)
  return Response.json({ count })
}

Output

[x] x() invoked
[y] y() invoked
[z] z() invoked
[z] 1
[y] 2
[x] 3
[y] y() invoked
[z] z() invoked
[z] 1
[y] 2

I’ve passed the questions to the devs.

1 Like

The devs mentioned, this is what is happening:

  1. We trigger X from the toml
  2. This triggers Y using context.next()
  3. This triggers Z using context.next()
  4. Y receives a response, which it logs
  5. Y doesn’t have a return statement, so it when it finishes executing it continues to the next function in the chain, which is Z
  6. X receives a response, which it logs
  7. X doesn’t have a return value, so it continues onto the next function in the chain and Y is invoked again
  8. Y calls Z using context.next()
  9. Y receives a response and logs it
  10. Y doesn’t have a return value, so we invoke the next function in the chain which is Z

And that’s the end of the chain… we’re not looping, we’re just continuing, which is why there’s no chain to break. And I think the way to make it behave as expected is by adding return to the end of the X and Y functions.

Awesome, thanks! Is there a git repo where something like this could possibly be added to the docs?

What exactly are you expecting the docs to document here?

I found the docs for context next confusing and creating an example as we have here with the steps listed out would have made it clear to me from the beginning. Even testing context next() gave confusing results. In the caching section, it lists out what happens when using stale while revalidate step by step and that was a great explanation. Doing something like what is in this thread and listing the steps could make the documentation for context next much clearer.