CodeGenieApp / serverless-express

Run Express and other Node.js frameworks on AWS Serverless technologies such as Lambda, API Gateway, Lambda@Edge, and more.
https://codegenie.codes
Apache License 2.0
5.17k stars 671 forks source link

Error: write EOF #251

Open shelooks16 opened 5 years ago

shelooks16 commented 5 years ago

Hi, I'm developing a Rest API with Nest.js. I successfully converted it to a monolithic lambda with aws-serverless-express with the next code:

const binaryMimeTypes: string[] = ['application/octet-stream'];

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  if (!cachedServer) {
    const expressApp = express();
    const nestApp = await NestFactory.create(
      AppModule,
      new ExpressAdapter(expressApp)
    );
    nestApp.use(eventContext());
    nestApp.enableCors();
    await nestApp.init();
    cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
  }
  return cachedServer;
}

export const handler: Handler = async (
  event: APIGatewayEvent,
  context: Context
) => {
  cachedServer = await bootstrapServer();
  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

For development, I use serverless-offline and serverless-webpack.

When I try to send an image with multipart/form-data to the /upload controller, it throws me an error regardless of the image type. ** With other file types (like .txt or .env) it works as expected. Before moving the app to lambda, it worked without any issues.

Controller:

 @Post('upload')
 @UseInterceptors(AnyFilesInterceptor())
 async upload(@UploadedFiles() files: any) {
    console.log('photos', files);
  }

Sending text file:

files [ { fieldname: 'file',
    originalname: 'expretiments.js',
    encoding: '7bit',
    mimetype: 'application/javascript',
    buffer:
     <Buffer 2f 2f 20 63 6f 6e 73 74 20 67 63 64 20 3d 20 28 61 2c 20 62 29 20 3d 3e 20 7b 0d 0a 2f 2f 20 09 77 68 69 6c 65 28 62 29 20 7b 0d 0
a 2f 2f 20 09 09 69 ... >,
    size: 8537 } ]

Error log (sending .png):

[Nest] 8276   - 09/20/2019, 12:18:42 PM   [ExceptionsHandler] Unexpected end of multipart data +149ms
Error: Unexpected end of multipart data
    at D:\web\_projects\new-book-2-wheel\server\node_modules\dicer\lib\Dicer.js:62:28
    at process._tickCallback (internal/process/next_tick.js:61:11)
ERROR: aws-serverless-express connection error
{ Error: write EOF
    at WriteWrap.afterWrite (net.js:788:14) errno: 'EOF', code: 'EOF', syscall: 'write' }

What I tried:

aws-serverless-express: 3.3.6 nodejs: v10.16.0 Current workaround: send images in base64 format

shelooks16 commented 5 years ago

As far as I understand, aws-serverless-express attaches multipart's Buffer as a string but multipart parser expects it to be of type Buffer.

Well, here is the code to make it work. I convert the body object (which is string) to Buffer and specify encoding as 'binary'. Handler:

export const handler: Handler = async (
  event: APIGatewayEvent,
  context: Context
) => {
  if (
    event.body &&
    event.headers['Content-Type'].includes('multipart/form-data')
  ) {
    // before => typeof event.body === string
    event.body = (Buffer.from(event.body, 'binary') as unknown) as string;
    // after => typeof event.body === <Buffer ...>
  }

  cachedServer = await bootstrapServer();
  return proxy(cachedServer, event, context, 'PROMISE').promise;
};
shelooks16 commented 5 years ago

Although the setup above works during development, in deployment it causes images being broken. I upload images to S3. Local upload is OK, when deployed - corrupted.

According to this post: https://stackoverflow.com/a/41770688, API gateway needs additional configuration to process binaries. To make it more or less automated, I simply installed serverless-apigw-binary and put */* wildcard.

serverless.yml:

plugins:
  - serverless-apigw-binary

custom:
  apigwBinary:
    types:
      - '*/*' 

handler:

const binaryMimeTypes: string[] = [];

if (
  event.body &&
  event.headers['Content-Type'].includes('multipart/form-data') &&
  process.env.NODE_ENV !== 'production' // added
) {
  event.body = (Buffer.from(event.body, 'binary') as unknown) as string;
}
prakashgitrepo commented 4 years ago

Hey @shelooks16 Could you share how you deployed this handler in serverlesss ? I'm followed the same procedure but it shows "errorMessage": "Cannot read property 'REQUEST' of undefined" while trying to access the api endpoint.

shelooks16 commented 4 years ago

Hey @shelooks16 Could you share how you deployed this handler in serverlesss ? I'm followed the same procedure but it shows "errorMessage": "Cannot read property 'REQUEST' of undefined" while trying to access the api endpoint.

Hey!! Here is full code for handler:

// index.ts

import { NestFactory } from '@nestjs/core';
import { Context, Handler, APIGatewayEvent } from 'aws-lambda';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { Server } from 'http';
import { ApiModule } from './api.module';
import { ExpressAdapter } from '@nestjs/platform-express';

// tslint:disable-next-line:no-var-requires
const express = require('express')();

const isProduction = process.env.NODE_ENV === 'production';
let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  return NestFactory.create(ApiModule, new ExpressAdapter(express))
    .then((nestApp) => {
      nestApp.use(eventContext());
      nestApp.enableCors();
      return nestApp.init();
    })
    .then(() => {
      return createServer(express);
    });
}

export const handler: Handler = async (
  event: APIGatewayEvent,
  context: Context
) => {
  if (
    isProduction &&
    // @ts-ignore
    event.source === 'serverless-plugin-warmup'
  ) {
    return 'Lambda is warm!';
  }

  if (
    !isProduction &&
    event.body &&
    event.headers['Content-Type'].includes('multipart/form-data')
  ) {
    event.body = (Buffer.from(event.body, 'binary') as unknown) as string;
  }

  if (!cachedServer) {
    cachedServer = await bootstrapServer();
  }

  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

I used serverless-webpack with next .base config:

// webpack.config.base.js

const path = require('path');
const nodeExternals = require('webpack-node-externals');
const _ = require('lodash');
const slsw = require('serverless-webpack');
require('source-map-support').install();

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const WebpackBar = require('webpackbar');

const rootDir = path.join(__dirname, '../'); // change it for your case
const buildDir = path.join(rootDir, '.webpack');
const isLocal = slsw.lib.webpack.isLocal;

const defaults = {
  mode: isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  target: 'node',
  externals: [nodeExternals()],
  node: {
    __filename: false,
    __dirname: false
  },
  optimization: {
    minimize: false
  },
  resolve: {
    extensions: ['.ts', '.js', '.json']
  },
  output: {
    libraryTarget: 'commonjs2',
    path: buildDir,
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          transpileOnly: true
        }
      },
      {
        test: /\.ts$/,
        enforce: 'pre',
        loader: 'tslint-loader'
      }
    ]
  },
  plugins: [new WebpackBar(), new ForkTsCheckerWebpackPlugin()]
};

module.exports.defaults = defaults;

module.exports.merge = function merge(config) {
  return _.merge({}, defaults, config);
};

Inside serverless config:

# serverless.yaml

plugins:
  - serverless-plugin-warmup
  - serverless-webpack
  - serverless-apigw-binary
  - serverless-prune-plugin
  - serverless-offline

# ...

custom:
  currentStage: ${opt:stage, 'dev'}
  webpack:
    webpackConfig: ./webpack/webpack.config.${self:custom.currentStage}.js
    packager: 'yarn'
    includeModules:
      forceInclude:
        - mysql2
      forceExclude:
        - aws-sdk
        - typescript
  apigwBinary:
    types:
      - 'multipart/form-data'
  prune:
    automatic: true
    number: 5
  warmup:
    prewarm: true
    concurrency: 2
    events:
      - schedule: 'cron(0/7 * ? * MON-FRI *)'

# ...

package:
  individually: true

functions:
  api:
    handler: src/api/index.handler
    warmup: true
    timeout: 30
    events:
      - http:
          path: /
          method: any
          cors: true
      - http:
          path: /{proxy+}
          method: any
          cors: true
calflegal commented 4 years ago

I am also experiencing the above issue, but with audio files: Unexpected end of multipart data

hbthegreat commented 4 years ago

@calflegal same with me here. Did you have any luck solving this?

calflegal commented 4 years ago

I did not :(. I moved to a dokku deploy, it stopped me from using serverless for now.

hbthegreat commented 4 years ago

@calflegal after digging super deep into it I realised that it was a problem I was facing with serverless-offline not handling multipart/form-data the same way as production apigateway+lambda does. So for local testing for file uploads I am running my regular non-serverless npm run style command. If you end up getting back onto this try setting apiGateway to accept multipart/form-data in your serverless.yml and you may have some success (just not locally)

calflegal commented 4 years ago

@hbthegreat that makes sense to me, nice job! Maybe worth connecting this issue with an issue there? FWIW, I had other issues in my app due to Safari's fetching strategy for audio (and I think video?) content, which, I was able to handle with one line of nginx config in my dokku setup, so I'm not coming back for now :)