|
- import {
- GetServerSideProps,
- NextApiHandler,
- NextApiRequest as DefaultNextApiRequest,
- NextApiResponse as DefaultNextApiResponse, PageConfig,
- } from 'next';
- import * as nookies from 'nookies';
- import {IncomingMessage} from 'http';
- import busboy from 'busboy';
- import {NextApiResponse, NextApiRequest} from './common';
-
-
- let BODY_COOKIE_KEY : string;
- let STATUS_CODE_COOKIE_KEY: string;
- let STATUS_MESSAGE_COOKIE_KEY: string;
-
- const generateBodyCookieKey = () => {
- BODY_COOKIE_KEY = `ifb${Date.now()}`;
- };
-
- const generateStatusCodeCookieKey = () => {
- STATUS_CODE_COOKIE_KEY = `ifsc${Date.now()}`;
- };
-
- const generateStatusMessageCookieKey = () => {
- STATUS_MESSAGE_COOKIE_KEY = `ifsm${Date.now()}`;
- };
-
- const getBody = (req: IncomingMessage) => new Promise<Buffer>((resolve, reject) => {
- let body = Buffer.from('');
-
- req.on('data', (chunk) => {
- body = Buffer.concat([body, chunk]);
- });
-
- req.on('error', (err) => {
- reject(err);
- });
-
- req.on('end', () => {
- resolve(body);
- });
- });
-
- export namespace destination {
- export const getServerSideProps = (gspFn?: GetServerSideProps): GetServerSideProps => async (ctx) => {
- const req: NextApiRequest = {
- query: ctx.query,
- };
- const { method = 'GET' } = ctx.req;
-
- if (!['GET', 'HEAD'].includes(method.toUpperCase())) {
- const body = await getBody(ctx.req);
- req.body = body.toString('utf-8');
- }
-
- const cookies = nookies.parseCookies(ctx);
- const res: NextApiResponse = {};
-
- if (STATUS_CODE_COOKIE_KEY in cookies) {
- ctx.res.statusCode = Number(cookies[STATUS_CODE_COOKIE_KEY] || '200');
- nookies.destroyCookie(ctx, STATUS_CODE_COOKIE_KEY, {
- path: '/',
- });
- }
-
- if (STATUS_MESSAGE_COOKIE_KEY in cookies) {
- ctx.res.statusMessage = cookies[STATUS_MESSAGE_COOKIE_KEY] || '';
- nookies.destroyCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, {
- path: '/',
- });
- }
-
- if (BODY_COOKIE_KEY in cookies) {
- const resBody = cookies[BODY_COOKIE_KEY];
- if (resBody.startsWith('json:')) {
- res.body = JSON.parse(resBody.slice(5));
- } else if (resBody.startsWith('raw:')) {
- res.body = resBody.slice(4);
- } else {
- console.warn('Could not parse response body, returning nothing');
- }
- nookies.destroyCookie(ctx, BODY_COOKIE_KEY, {
- path: '/',
- });
- }
-
- let gspResult;
- if (gspFn) {
- gspResult = await gspFn(ctx)
- } else {
- gspResult = {
- props: {},
- };
- }
- if ('props' in gspResult) {
- return {
- ...gspResult,
- props: {
- ...gspResult.props,
- req,
- res,
- },
- };
- }
-
- // redirect/not found will be treated as default behavior
- return gspResult;
- };
- }
-
- export namespace action {
- export const getApiConfig = (customConfig = {} as PageConfig) => ({
- api: {
- ...(customConfig.api ?? {}),
- bodyParser: false,
- },
- });
-
- export const wrapApiHandler = (fn: NextApiHandler): NextApiHandler => async (req, res) => {
- const body = await deserializeBody(req);
- const reqMut = req as unknown as Record<string, unknown>;
- reqMut.body = body;
- return fn(reqMut as unknown as DefaultNextApiRequest, res);
- };
-
- const parseMultipartFormData = async (req: IncomingMessage) => {
- return new Promise<Record<string, unknown>>((resolve, reject) => {
- const body: Record<string, unknown> = {};
- const bb = busboy({
- headers: req.headers,
- });
-
- bb.on('file', (name, file, info) => {
- const {
- filename,
- mimeType: mimetype
- } = info;
-
- let fileData = Buffer.from('');
-
- file.on('data', (data) => {
- fileData = Buffer.concat([fileData, data]);
- });
-
- file.on('close', () => {
- body[name] = new File([fileData.buffer], filename, {
- type: mimetype,
- });
- });
- });
-
- bb.on('field', (name, value) => {
- body[name] = value;
- });
-
- bb.on('close', () => {
- resolve(body);
- });
-
- bb.on('error', (error) => {
- reject(error);
- });
-
- req.pipe(bb);
- });
- };
-
- const deserializeBody = async (req: IncomingMessage) => {
- const contentType = req.headers['content-type'];
- // TODO get body encoding from headers
- const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding;
- // should we turn off default body parsing?
- if (contentType === 'application/json') {
- const bodyRaw = await getBody(req);
- return JSON.parse(bodyRaw.toString(encoding));
- }
- if (contentType === 'application/x-www-form-urlencoded') {
- const bodyRaw = await getBody(req);
- return Object.fromEntries(
- new URLSearchParams(bodyRaw.toString(encoding)).entries()
- );
- }
- if (contentType?.startsWith('multipart/form-data;')) {
- return parseMultipartFormData(req);
- }
- const bodyRaw = await getBody(req);
- return bodyRaw.toString('binary');
- };
-
- export const getServerSideProps = (fn: NextApiHandler): GetServerSideProps => async (ctx) => {
- const { referer } = ctx.req.headers;
-
- const mockReq = {
- ...ctx.req,
- body: await deserializeBody(ctx.req),
- } as DefaultNextApiRequest;
-
- let data = null;
- const mockRes = {
- // todo handle other nextapiresponse methods (e.g. setting headers, writeHead, etc.)
- statusMessage: '',
- statusCode: 200,
- status(code: number) {
- // should we mask error status code to Bad Gateway?
- this.statusCode = code;
- return mockRes;
- },
- send: (raw: any) => {
- // todo: how to transfer binary response in a more compact way?
- data = `raw:${raw.toString('base64')}`;
- },
- json: (raw: any) => {
- data = `json:${JSON.stringify(raw)}`;
- },
- } as DefaultNextApiResponse;
-
- await fn(mockReq, mockRes);
-
- generateStatusCodeCookieKey();
- nookies.setCookie(ctx, STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString(), {
- maxAge: 30 * 24 * 60 * 60,
- path: '/',
- });
-
- generateStatusMessageCookieKey();
- nookies.setCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage, {
- maxAge: 30 * 24 * 60 * 60,
- path: '/',
- });
-
- if (data) {
- generateBodyCookieKey();
- nookies.setCookie(ctx, BODY_COOKIE_KEY, data, {
- maxAge: 30 * 24 * 60 * 60,
- path: '/',
- });
- }
-
- return {
- redirect: {
- destination: referer,
- statusCode: 307,
- },
- props: {
- query: ctx.query,
- body: data,
- },
- };
- };
- }
|