opennextjs / opennextjs-aws

Open-source Next.js adapter for AWS
https://opennext.js.org
MIT License
4.14k stars 126 forks source link

Empty response when using http-proxy-middleware in an api route #517

Closed sladkoff closed 2 months ago

sladkoff commented 2 months ago

Ok, so this one feels special. I hope I got all the evidence right...

Versions

Setup

I have the following api route handler:

/api/example/[...proxy].ts


export const config = {
  api: {
    bodyParser: false,
    externalResolver: true,
  },
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  console.log("test handler")

  const proxy = createProxyMiddleware<NextApiRequest, NextApiResponse>({
    target: "https://jsonplaceholder.typicode.com/comments",
    pathRewrite: {
      '^/api/example': '',
    },
    changeOrigin: true,
    logger: console,
  })

  // FIXME seems not to be called properly in AWS / open-next (works locally in standalone server and in local open-next)
  return await proxy(req, res, (result: unknown) => {
    if (result instanceof Error) {
      throw result
    }
  })
}

Expected behaviour

When running this api route in standalone nextjs mode, it works as expected and returns the full JSON at http://localhost:3000/api/example/comments

This also works with a local open-next build as far as I can tell.

Actual behavior on AWS

Requesting the same api path returns a blank page / response with content-length: 0; content-type: application/json.

CloudWatch Logs Insights

Note: Invocation 2717fc6e-9f58-4dc4-9766-f6f1e02695ee logged the proxy invocation and returned an empty body. It seems that the next invocation d1f54afb-a874-459b-8c84-ab6294752bc4 contains a socket hangup error from the proxy library which suggests that the response of the previous invocation returned before the proxy call was complete.

cloudwatch-logs.md

Other context

I tried several configurations of http-proxy-middleware, http-proxy as well as next-http-proxy-middleware with no success.

Since this works in open-next locally when using converter: 'node', I'm suspecting that this has something to do with Lambda ror the converter: aws-apigw-v2 that is used on AWS.

conico974 commented 2 months ago

Are you sure that await proxy is awaiting for the end of the response ? My guess is that it's not and this would explain why it works with a long running server and not in lambda. You'll have to properly await for the response to be done before returning.

sladkoff commented 2 months ago

Are you sure that await proxy is awaiting for the end of the response ? My guess is that it's not and this would explain why it works with a long running server and not in lambda. You'll have to properly await for the response to be done before returning.

I'm not sure about that. I have used the "official" recipe for NextJS that is provided by the http-proxy-middleware lib. That recipe doesn't use async at all.

I'll try to figure out if there's a way to await.

sladkoff commented 2 months ago

@conico974 You were right. The http-proxy and related libraries are not very promise-friendly. The following setup allows us to await until the response is completely proxied:

import { createProxyMiddleware } from 'http-proxy-middleware'

const httpProxyMiddleware = async (
  req: NextApiRequest,
  res: NextApiResponse,
): Promise<any> => new Promise<void>((resolve, reject) => {
  const proxy = createProxyMiddleware<NextApiRequest, NextApiResponse>({
    target: 'http://jsonplaceholder.typicode.com',
    changeOrigin: true,
    pathRewrite: {
      '^/api/comments': '/comments',
    },
    on: {
      proxyRes: (proxyRes, req, res) => {
        proxyRes.on('end', () => {
          console.log('Proxy request done')
          resolve() // <--We have to resolve here.
        })
      },
    },
  })
  proxy(req, res, (err) => {
    if (err) {
      return reject(err)
    }
  })
})

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {

  await httpProxyMiddleware(req, res)
  console.log('API Route handler done')
}

This example doesn't contain proper error handling