Serverless-express: Error: write EOF

Created on 20 Sep 2019  路  9Comments  路  Source: vendia/serverless-express

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:

  • changing multer to express-form-data
  • playing around with binaryMimeTypes passed to createServer()
  • using aws-lambda-multipart-parser package

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

Most helpful comment

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;
}

All 9 comments

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;
};

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;
}

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 @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

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

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

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

@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)

@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 :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

muswain picture muswain  路  8Comments

bansalrachita picture bansalrachita  路  4Comments

donny08 picture donny08  路  8Comments

goldenbearkin picture goldenbearkin  路  9Comments

tallpants picture tallpants  路  3Comments