dherault / serverless-offline

Emulate AWS λ and API Gateway locally when developing your Serverless project
MIT License
5.16k stars 794 forks source link

Support for Lambda Response Streaming #1681

Open serg06 opened 1 year ago

serg06 commented 1 year ago

Feature Request

Lambda has just released Lambda Response Streaming! It uses Transfer-Encoding: chunked to stream data back to the client. It's an extremely useful feature for my team, and I wish we could test it offline.

Note: It only works through Lambda Function URLs, but serverless-offline doesn't support them.

If a solution is not possible, then a workaround would be highly appreciated

Sample Code

service: my-service

plugins:
  - serverless-offline

provider:
  runtime: nodejs18.x
  stage: dev

functions:
  stream:
    handler: handler
    url: true  # Ideal option - Lambda URL
    events:  # Backup option - API gateway
      - http:
          method: ANY
          path: 'stream/{proxy+}'
      - http:
          method: ANY
          path: /stream/

resources:
  extensions:
    StreamLambdaFunctionUrl:
      Properties:
        InvokeMode: RESPONSE_STREAM
// Example 1
exports.handler = awslambda.streamifyResponse(
    async (event, responseStream, context) => {
        responseStream.setContentType(“text/plain”);
        responseStream.write(“Hello, world!”);
        responseStream.end();
    }
);

// Example 2
exports.handler = awslambda.streamifyResponse(async (event, responseStream, context) => {
  const httpResponseMetadata = {
    statusCode: 200,
    headers: {
      'Content-Type': 'text/html',
      'X-Custom-Header': 'Example-Custom-Header',
    },
  };

  responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);

  responseStream.write('<html>');
  responseStream.write('<p>First write2!</p>');

  responseStream.write('<h1>Streaming h1</h1>');
  await new Promise((r) => setTimeout(r, 1000));
  responseStream.write('<h2>Streaming h2</h2>');
  await new Promise((r) => setTimeout(r, 1000));
  responseStream.write('<h3>Streaming h3</h3>');
  await new Promise((r) => setTimeout(r, 1000));

  const loremIpsum1 =
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vitae mi tincidunt tellus ultricies dignissim id et diam. Morbi pharetra eu nisi et finibus. Vivamus diam nulla, vulputate et nisl cursus, pellentesque vehicula libero. Cras imperdiet lorem ante, non posuere dolor sollicitudin a. Vestibulum ipsum lacus, blandit nec augue id, lobortis dictum urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi auctor orci eget tellus aliquam, non maximus massa porta. In diam ante, pulvinar aliquam nisl non, elementum hendrerit sapien. Vestibulum massa nunc, mattis non congue vitae, placerat in quam. Nam vulputate lectus metus, et dignissim erat varius a.';
  responseStream.write(`<p>${loremIpsum1}</p>`);
  await new Promise((r) => setTimeout(r, 1000));

  responseStream.write('<p>DONE!</p>');
  responseStream.end();
});

Expected behavior/code

Currently serverless-offline doesn't support Lambda URLs, so I have to do it through API gateway. This is what happens:

ANY /dev/stream (λ: stream)
× Unhandled exception in handler 'stream'.
× TypeError: underlyingStream.setContentType is not a function
      at HttpResponseStream2.from (C:\monorepo\node_modules\serverless-offline\src\lambda\handler-runner\in-process-runner\aws-lambda-ric\UserFunction.js:157:28)        
      at C:\monorepo\apps\server\.esbuild\.build\src\functions\stream.js:1426:49    
      at InProcessRunner.run (file:///C:/monorepo/node_modules/serverless-offline/src/lambda/handler-runner/in-process-runner/InProcessRunner.js:87:20)
      at async HandlerRunner.run (file:///C:/monorepo/node_modules/serverless-offline/src/lambda/handler-runner/HandlerRunner.js:114:14)
      at async LambdaFunction.runHandler (file:///C:/monorepo/node_modules/serverless-offline/src/lambda/LambdaFunction.js:305:16)
      at async file:///C:/monorepo/node_modules/serverless-offline/src/events/http/HttpServer.js:602:18
      at async exports.Manager.execute (C:\monorepo\node_modules\@hapi\hapi\lib\toolkit.js:60:28)
      at async internals.handler (C:\monorepo\node_modules\@hapi\hapi\lib\handler.js:46:20)
      at async exports.execute (C:\monorepo\node_modules\@hapi\hapi\lib\handler.js:31:20)
      at async Request._lifecycle (C:\monorepo\node_modules\@hapi\hapi\lib\request.js:370:32)
      at async Request._execute (C:\monorepo\node_modules\@hapi\hapi\lib\request.js:280:9)
× underlyingStream.setContentType is not a function

Ideal solution: It provides me with a Lambda URL to use.

Minimal solution: I'm able to use it through API Gateway.

Additional context/Screenshots

Here are the contents of UserFunction.js: https://gist.github.com/serg06/0653a4c690a7f5854f8038e3ebe62a64

grakic commented 1 year ago

As serverless-offline added experimental supports for ALB and not just API Gateway, it could be argued that support for Lambda Function URLs could fit inside this project scope. Many users may be migrating from REST/HTTP API Gateway to Function URLs, and there seems to be no valid alternatives for local development today.

To simulate AWS environment, Lambda Function URLs do require hostname or port based routing, as when function is exposed via Function URL, any HTTP method and request path invokes the Lambda function. Adding path-prefix in serverless-offline to expose different functions will introduce different behavior than the simulated AWS environment.

Lambda Function URL can be set in buffered or streaming mode.

To support streaming mode in serverless-offline with Lambda In-Process runner we have to:

Based on some tests done in AWS environment, both decorated and un-decorated handlers can be invoked via function URLs in both invoke modes.

I am slowly working on this to allow local development for streaming responses.

harounansari-cj commented 1 year ago

Any update on this?

PierrickLozach commented 5 months ago

Any updates on this? I can see that RESPONSE_STREAM is supported however I get the same error when trying to call the function locally using serverless-offline: TypeError: underlyingStream.setContentType is not a function.

Thanks!

asychev commented 5 months ago

+1

rubenbaraut commented 5 months ago

+1 Same problem here. Trying with serverless-offline-lambda-function-urls plugin, and is failing too.

ekarmazin commented 4 months ago

+1 Same problem:

"errorMessage": "underlyingStream.setContentType is not a function",
    "errorType": "TypeError",
    "stackTrace": [
        "TypeError: underlyingStream.setContentType is not a function"
jayarjo commented 4 months ago

Any movement on this?