Error: write EOF
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
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 :)