serverless-offline icon indicating copy to clipboard operation
serverless-offline copied to clipboard

Support for Lambda Response Streaming

Open serg06 opened this issue 2 years ago • 10 comments

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

  • file: serverless.yml
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
  • file: handler.js
// 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

serg06 avatar Apr 09 '23 21:04 serg06

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:

  • expose additional hapi server with host-based routing to support multiple Lambda URLs. This is based on discussion in https://github.com/dherault/serverless-offline/issues/1382
  • define exposed Lambda URLs as buffering or streaming based on https://github.com/serverless/serverless/issues/11906
  • extend LambdaFunction with option to set hapi response stream
  • after loading handler in Lambda In-Process runner check for symbol set by awslambda.streamifyResponse() and call it passing hapi response stream as underlaying stream

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.

grakic avatar Apr 10 '23 20:04 grakic

Any update on this?

harounansari-cj avatar Jun 18 '23 15:06 harounansari-cj

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!

PierrickLozach avatar Jan 11 '24 10:01 PierrickLozach

+1

asychev avatar Jan 18 '24 14:01 asychev

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

rubenbaraut avatar Jan 26 '24 16:01 rubenbaraut

+1 Same problem:

"errorMessage": "underlyingStream.setContentType is not a function",
    "errorType": "TypeError",
    "stackTrace": [
        "TypeError: underlyingStream.setContentType is not a function"

ekarmazin avatar Feb 27 '24 05:02 ekarmazin

Any movement on this?

jayarjo avatar Mar 09 '24 08:03 jayarjo