apollo-server-micro package tries to receive request body stream that already received before(in some scenarios) and hangs on forever cause never receives events of stream(it already fully obtained)
I gone step by step through all the flow to discover the issue.
In brief the issue pops up when request stream that passed to Apollo already read before. It means we already had used for example: body.on('data', onData) & body.on('end', onEnd) or it was executed by another chain in the flow(express server, next.js server, firebase cloud function).
And if it was before what is going in apollo-server-micro code is that it tries to do it again, but it will never occur and we will fail on timeout or never get the response, because: body.on('data', or body.on('end' will never be called again(the stream already parsed before fully, these event will not happen).
So I think some way is needed to treat this situation and to give Apollo option to work with body stream already received. May be we need some way to say Apollo to not try to receive body stream if it already exists and just deliver it already prepared(buffer) by some property. So we don't need to do it if I already can provide it to the Apollo.
I found some hack I can do, but by far it needed to be done in more proper way.
Apollo uses json function from micro package (https://github.com/vercel/micro) to get the body stream. if I change this line there:
const body = rawBodyMap.get(req);
to something like:
const body = rawBodyMap.get(req) || req.rawBody;
I have rawBody because I use firebase cloud function and when it receives body stream it saves received stream buffer in rawBody property of request (and it's exactly what json function of micro tries to achieve).
full flow:
src/ApolloServer.ts ( from apollo-server-micro package )
import { graphqlMicro } from './microApollo';
const graphqlHandler = graphqlMicro(() => {
return this.createGraphQLServerOptions(req, res);
});
const responseData = await graphqlHandler(req, res);
send(res, 200, responseData);
microApollo.ts - we use here json function from 'micro' passing req as parameter
import { send, json, RequestHandler } from 'micro'; ( https://github.com/vercel/micro )
const graphqlHandler = async (req: MicroRequest, res: ServerResponse) => {
let query;
try {
query =
req.method === 'POST'
? req.filePayload || (await json(req))
: url.parse(req.url, true).query;
} catch (error) {
// Do nothing; `query` stays `undefined`
}
https://github.com/vercel/micro package
const getRawBody = require('raw-body');
exports.json = (req, opts) =>
exports.text(req, opts).then(body => parseJSON(body));
exports.text = (req, {limit, encoding} = {}) =>
exports.buffer(req, {limit, encoding}).then(body => body.toString(encoding));
exports.buffer = (req, {limit = '1mb', encoding} = {}) =>
Promise.resolve().then(() => {
const type = req.headers['content-type'] || 'text/plain';
const length = req.headers['content-length'];
// eslint-disable-next-line no-undefined
if (encoding === undefined) {
encoding = contentType.parse(type).parameters.charset;
}
// my try to hack the behavior
const body = rawBodyMap.get(req) || req.rawBody;
console.log(">>>>>>>>>>>>>>>>>>> ", body);
if (body) {
return body;
}
return getRawBody(req, {limit, length, encoding})
.then(buf => {
rawBodyMap.set(req, buf);
return buf;
})
.catch(err => {
if (err.type === 'entity.too.large') {
throw createError(413, `Body exceeded ${limit} limit`, err);
} else {
throw createError(400, 'Invalid body', err);
}
});
});
if I don't stop by my hack the code from going to receive the body
stream it calls to : getRawBody from 'raw-body' package;
raw-body package
function getRawBody (stream, options, callback) {
……
return new Promise(function executor (resolve, reject) {
readStream(stream, encoding, length, limit, function onRead (err, buf) {
if (err) return reject(err)
resolve(buf)
})
})
}
function readStream (stream, encoding, length, limit, callback) {
…….
// attach listeners
// these callbacks never called because body request stream already received before
stream.on('aborted', onAborted)
stream.on('close', cleanup)
stream.on('data', onData)
stream.on('end', onEnd)
stream.on('error', onEnd)
…….
See Question&Answers more detail:
os