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

Error: write EOF

Open shelooks16 opened this issue 6 years ago • 9 comments

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

shelooks16 avatar Sep 20 '19 10:09 shelooks16

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 avatar Sep 22 '19 14:09 shelooks16

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

shelooks16 avatar Sep 24 '19 20:09 shelooks16

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.

prakashgitrepo avatar Mar 12 '20 19:03 prakashgitrepo

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

shelooks16 avatar Mar 17 '20 15:03 shelooks16

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

calflegal avatar Apr 11 '20 14:04 calflegal

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

hbthegreat avatar Apr 15 '20 16:04 hbthegreat

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

calflegal avatar Apr 15 '20 16:04 calflegal

@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 avatar Apr 16 '20 06:04 hbthegreat

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

calflegal avatar Apr 16 '20 12:04 calflegal