neondatabase / serverless

Connect to Neon PostgreSQL from serverless/worker/edge functions
https://www.npmjs.com/package/@neondatabase/serverless
MIT License
321 stars 13 forks source link

Error in cloudflare workers : Uncaught (in response) Error: The script will never generate a response #11

Closed mrcndn closed 1 year ago

mrcndn commented 1 year ago

Hi I’m getting the following error in Cloudflare workers

A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished. ✘ [ERROR] Uncaught (in response) Error: The script will never generate a response.

My code is simple

import { Client } from '@neondatabase/serverless'
import { Env } from './models'

export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext) {
        const client = new Client({/*details*/})
        await client.connect()
        const {
            rows: [{ now }]
        } = await client.query('select now();')
        ctx.waitUntil(client.end())
        return new Response(now)
    }
}

I have found the following page but it does not mention any solution "The script will never generate a response” on CloudFlare Workers | Zuplo Docs

Following code still getting the error.

await client.connect()
/*

const {
    rows: [{ now }]
} = await client.query('select now();')
ctx.waitUntil(client.end())
*/

return new Response('eeee')

After comment out await client.connect() the code starting to run

jawj commented 1 year ago

That's odd, because your example looks just right.

I would expect just that behaviour if you didn't call connect(), but not if you did (is there any chance there's a caching issue here, and the two cases are getting confused?).

Incidentally, in more recent docs and examples we use Pool instead of Client, and in that case there's no need to call connect().

Can you perhaps share your connection {/*details*/}? Minus the password, of course! If it's a Neon DB, that would usually be a connection string/URL like postgres://user:pass@abc.def.neon.tech/neondb.

retrohacker commented 1 year ago

This happens to me with the following code snippet:

const pool = new Pool({ connectionString: env.DB_CONNECTION })
const client = await pool.connect()
console.log("this is never reached")

The result is this error:

Uncaught (in response) Error: The script will never generate a response.
A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished.

The reason was env.DB_CONNECTION was undefined.

If you are running the local version of npx wrangler dev (either typing l in the interactive prompt or passing the -l flag to the cli) you need to put your environment variables in .dev.vars.

jawj commented 1 year ago

Right, it's certainly true that you need your environment variables in .dev.vars for local dev with wrangler. I think it may also be true that when we pipeline the pg connection, some errors are not being thrown when they should be. It's on my list to investigate and hopefully fix (and at the very least document) this.

joshiain commented 1 year ago

I'm having the same issue with Cloudflare Pages, but it's quite intermittent. ~50% of the time data will load properly, the rest of the time it will error with these errors. Locally it works completely fine. I'm using Kysely with the Neon Serverless driver.

I also tested my app using the Planetscale Serverless driver to see if it could be something else with my app, but I didn't get these errors using Planetscale

jawj commented 1 year ago

Intermittent failure is quite surprising. Are you using the latest driver version? I changed the error handling in connect recently, which may help these errors be reported more helpfully.

joshiain commented 1 year ago

Yea I'm using the latest driver version. They look as follows when I turn on the log stream in Cloudflare Pages

image

They tend to error more often than not, but refreshing or clicking a few times and the data eventually loads successfully

jawj commented 1 year ago

@joshiain Can I check that you're either doing new Client() + connect(), or new Pool() with no connect()? And can I check that you're doing that fully within the request handler, and not trying to keep a Client or Pool alive between requests?

If the answer is yes to both, then could you share a minimal repro that shows this issue?

joshiain commented 1 year ago

@jawj I'm using new Pool() with no connect(), but I am trying to keep the Pool alive between requests by instantiating it outside of the handler, as I thought this would improve performance, is this not the case? Should I be instantiating a new Client/Pool each request?

jawj commented 1 year ago

Right — if it worked to keep a Pool alive between requests, that would be great for performance! Unfortunately Cloudflare Workers can't currently support that. We have however been working on making establishing new connections as fast as possible.

joshiain commented 1 year ago

That's interesting that Cloudflare Workers doesn't support it. Wondering why the Planetscale Serverless driver doesn't have these issues when I instantiate it in the same way?

While I may be trying to keep the pool alive between requests in my code, maybe SvelteKit is compiling it so that everything is done fully inside the request handler for a Cloudflare Worker deployment target?

jawj commented 1 year ago

Neon's serverless driver takes node-postgres (pg) and replaces the usual TCP connection with a WebSocket connection. This gives drop-in compatibility with node-postgres.

I believe the PlanetScale driver is its own thing, and communicates with the backend over simple http. Presumably this doesn't have the same requirements on keeping a connection alive.

ben-xD commented 1 year ago

I think this is highly related to the issue I reported on drizzle: https://github.com/drizzle-team/drizzle-orm/issues/619. Could I get some help on that?

pyrossh commented 1 year ago

Facing the same issue using drizzle-orm and neon-serverless using pool on cloudflare workers locally. It seems 50% of the requests fail with this error. Here is a more detailed error,

✘ [ERROR] Uncaught Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance.

this.ws.send(this.writeBuffer), this.writeBuffer = void 0; ^

ben-xD commented 1 year ago

@pyrossh, you should share some code. Each invocation of Cloudflare Workers is separate/isolated. You shouldn't try to share a database client between requests.

pyrossh commented 1 year ago

@ben-xD yes you are right. It seems the wrangler pages dev tool is reusing the same client across requests instead of rerunning the script again. It seems to be a bug on their tool. Anyways will move to miniflare soon and that will fix it. Unfortunately I'm not able to test it in cloudflare pages as my build doesn't work there for some odd reason.

Here is the code if you are interested, https://github.com/pyrossh/edge-city/blob/main/example/src/db/index.js https://github.com/pyrossh/edge-city/blob/main/example/src/services/todos.service.js#L24

You can see in this log that the client id doesn't change across 2 different /todos requests.

[mf:inf] GET /service-worker.js 404 Not Found (3ms)
DB CLIENT 98.00879592437293
select "id", "text", "completed", "createdAt", "updatedAt" from "todos" order by "todos"."id" asc
[mf:inf] GET /todos 200 OK (6ms)
[mf:inf] GET /css/app.css 304 Not Modified (6ms)
When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.
When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.
When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.
When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.
When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.
[mf:inf] GET /js/todos.js 304 Not Modified (4ms)
[mf:inf] GET /js/chunks/chunk-7LRUF4ED.js 304 Not Modified (3ms)
[mf:inf] GET /js/chunks/chunk-NZ2AH5VX.js 304 Not Modified (8ms)
[mf:inf] GET /js/chunks/chunk-ASIA2CUG.js 304 Not Modified (9ms)
[mf:inf] GET /js/chunks/client-FJZY6L7Z.js 304 Not Modified (5ms)
[mf:inf] GET /favicon.ico 304 Not Modified (4ms)
[mf:inf] GET /js/todos.js 304 Not Modified (3ms)
[mf:inf] GET /service-worker.js 404 Not Found (1ms)
[mf:inf] GET /todos 200 OK (4ms)
DB CLIENT 98.00879592437293
select "id", "text", "completed", "createdAt", "updatedAt" from "todos" order by "todos"."id" asc
A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished.
workerd/server/server.c++:2347: error: Uncaught exception: workerd/io/io-context.c++:1261: failed: remote.jsg.Error: The script will never generate a response.
stack: 100ad96e4 100adab48 100adb0cc 101f356d0 1010ab9cf 1010aa398 10083d5e8 100be3c50 100be3fe3 100adb0cc 10106fb98 101072610
[mf:inf] GET /css/app.css 304 Not Modified (5ms)
[mf:inf] GET /favicon.ico 304 Not Modified (3ms)
✘ [ERROR] Uncaught Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance.

  this.ws.send(this.writeBuffer), this.writeBuffer = void 0;
          ^
      at  (/private/Users/pyrossh/Code/Mine/edge-city/example/build/functions/todos.js:42353:22)

✘ [ERROR] Uncaught (async) Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance.

✘ [ERROR] Uncaught (in response) Error: The script will never generate a response.
ben-xD commented 1 year ago

Oh @pyrossh, you shouldn't instantiate a pool like https://github.com/pyrossh/edge-city/blob/164eba9f3929acbb4bbdab4d59ec2db365457361/example/src/services/db.js#L5. You should instantiate when the request comes in. This is somewhat related to https://zuplo.com/blog/the-script-will-never-generate-a-response-on-cloudflare-workers/

You should create the pool in the fetch handler. I use tRPC, so I generate it in the createContext. For example:

// index.ts
import {
  FetchCreateContextFnOptions,
  fetchRequestHandler,
} from "@trpc/server/adapters/fetch";
import { appRouter } from "./trpc/appRouter";
import { trpcApiPath } from "./trpc/trpcPath";
import { Env } from "./env";
import { createContext } from "./trpc/context";

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 200,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Headers": "*",
        },
      });
    }

    return fetchRequestHandler({
      endpoint: trpcApiPath,
      req: request,
      router: appRouter,
      createContext: (options: FetchCreateContextFnOptions) =>
        createContext({ ...options, env, ctx }),
    });
  },
};

createContext

import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { inferAsyncReturnType } from "@trpc/server";
import { Client as NeonClient } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-serverless";
import { Env } from "../env";

export const createContext = async ({
  req,
  env,
  resHeaders,
  ctx: cfCtx,
}: FetchCreateContextFnOptions & { env: Env; ctx: ExecutionContext }) => {
  // let requestHeaders = JSON.stringify([...req.headers]);
  // logger.info(requestHeaders);

  // https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/
  const clientIp = req.headers.get("cf-connecting-ip");

  // Alternatively, get an environment variable with: `import.meta.env.SERVER_URL`
  // You can read custom or pre-defined environmment variables with
  // e.g. import.meta.env.MODE, .BASE_URL, .CUSTOM_VAR, etc.

  // Follow https://github.com/drizzle-team/drizzle-orm/blob/main/examples/neon-cloudflare/src/index.ts#L26
  // TODO Cloudflare is adding support for raw TCP, so we can connect using
  // Unix sockets?: see https://node-postgres.com/features/connecting#unix-domain-sockets

  const neonClient = new NeonClient(env.DATABASE_URL);
  // It's really weird that we have to call `connect()` here, but we do. We don't have the `neonClient` in procedures.
  // If we don't call `connect()` here, it errors with https://github.com/drizzle-team/drizzle-orm/issues/619. If you use Pool, you don't need to call connect(). See https://github.com/neondatabase/serverless/issues/11#issuecomment-1476010672
  neonClient.connect();
  const db = drizzle(neonClient, { logger: true });

  // TODO Consider moving database client creation to middleware, so different
  // procedures can choose between database Pool or Client.
  // UPDATE: Actually you can just get 1 client from the pool, which is
  // useful for parallel queries. See https://node-postgres.com/features/pooling#examples
  // const dbPool = new NeonPool({ connectionString: env.DATABASE_URL });
  // const db = drizzle(dbPool, { logger: true });

  return {
    env,
    req,
    resHeaders,
    db,
    neonClient,
    clientIp,
    cfCtx,
  };
};

export type Context = inferAsyncReturnType<typeof createContext>;

I hope that helps :)

pyrossh commented 1 year ago

@ben-xD That worked. Thanks 👍 . I don't see that error anymore.