Fix: Inconsistent JSON parsing and formatting in res.body and Res-preview (#4103)

* Fix: Revert selective JSON parsing where string response is not parsed

- Revert "Merge pull request #3706 from Pragadesh-45/fix/response-format-updates"
  - e897dc1eb0
- Revert "Merge pull request #3676 from pooja-bruno/fix/string-json-response"
  - 1f2bee1f90

* Fix: Revert interpreting Assert RHS-value wrapped in quotes literally

- Revert "Merge pull request #3806 from Pragadesh-45/fix/handle-assert-results"
  - 63d3cb380d
- Revert "Merge pull request #3805 from Pragadesh-45/fix/handle-assert-results"
  - 6abd063749

* Fix: Inconsistent JSON formatting in preview when encoded value is a string

* Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings

* Fix(testbench): req.body is always Buffer after the binary req body related changes

* Added `/api/echo/custom` where response can be configured using request itself

* Added tests for validating Assert and Response-preview

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>

* Handle char-encoding in Response-preview and added more tests

* Updated API endpoint in tests to use httpfaker api

* QuickJS (Safe Mode) exec logic to handle template literals similar to Developer Mode

* Safe Mode bru.runRequest to return statusText similar to Developer Mode

---------

Co-authored-by: ramki-bruno <ramki@usebruno.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
Pragadesh-45
2025-03-13 00:49:57 +05:30
committed by GitHub
parent 0fbbe8a996
commit 6a85635c49
35 changed files with 669 additions and 53 deletions

1
package-lock.json generated
View File

@@ -24447,6 +24447,7 @@
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",

View File

@@ -36,6 +36,7 @@
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",

View File

@@ -3,45 +3,49 @@ import QueryResultFilter from './QueryResultFilter';
import { JSONPath } from 'jsonpath-plus';
import React from 'react';
import classnames from 'classnames';
import iconv from 'iconv-lite';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
import QueryResultPreview from './QueryResultPreview';
import StyledWrapper from './StyledWrapper';
import { useState, useMemo, useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
import { uuid } from 'utils/common/index';
import { getEncoding, prettifyJson, uuid } from 'utils/common/index';
const formatResponse = (data, mode, filter) => {
if (data === undefined) {
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
if (data === undefined || !dataBuffer) {
return '';
}
if (data === null) {
return 'null';
}
// TODO: We need a better way to get the raw response-data here instead
// of using this dataBuffer param.
// Also, we only need the raw response-data and content-type to show the preview.
const rawData = iconv.decode(
Buffer.from(dataBuffer, "base64"),
iconv.encodingExists(encoding) ? encoding : "utf-8"
);
if (mode.includes('json')) {
let isValidJSON = false;
try {
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
JSON.parse(rawData);
} catch (error) {
console.log('Error parsing JSON: ', error.message);
}
if (!isValidJSON && typeof data === 'string') {
return data;
// If the response content-type is JSON and it fails parsing, its an invalid JSON.
// In that case, just show the response as it is in the preview.
return rawData;
}
if (filter) {
try {
data = JSONPath({ path: filter, json: data });
return prettifyJson(JSON.stringify(data));
} catch (e) {
console.warn('Could not apply JSONPath filter:', e.message);
}
}
return safeStringifyJSON(data, true);
// Prettify the JSON string directly instead of parse->stringify to avoid
// issues like rounding numbers bigger than Number.MAX_SAFE_INTEGER etc.
return prettifyJson(rawData);
}
if (mode.includes('xml')) {
@@ -56,7 +60,7 @@ const formatResponse = (data, mode, filter) => {
return data;
}
return safeStringifyJSON(data, true);
return prettifyJson(rawData);
};
const formatErrorMessage = (error) => {
@@ -76,7 +80,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
const formattedData = formatResponse(data, mode, filter);
const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter);
const { displayedTheme } = useTheme();
const debouncedResultFilterOnChange = debounce((e) => {

View File

@@ -1,5 +1,6 @@
import { customAlphabet } from 'nanoid';
import xmlFormat from 'xml-formatter';
import { format as jsoncFormat, applyEdits as jsoncApplyEdits } from 'jsonc-parser';
// a customized version of nanoid without using _ and -
export const uuid = () => {
@@ -26,6 +27,13 @@ export const waitForNextTick = () => {
});
};
export const prettifyJson = (doc) => {
return jsoncApplyEdits(
doc,
jsoncFormat(doc, null, {insertSpaces: true, tabSize: 2})
);
}
export const safeParseJSON = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
@@ -176,3 +184,9 @@ export const generateUidBasedOnHash = (str) => {
};
export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
export const getEncoding = (headers) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers['content-type'] || '');
return charsetMatch?.[1];
}

View File

@@ -34,14 +34,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) {
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {
}
} catch { }
return { data, dataBuffer };
};

View File

@@ -398,14 +398,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) {
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {
console.log('Failed to parse response data as JSON');
}
} catch { }
return { data, dataBuffer };
};

View File

@@ -22,6 +22,12 @@ const toNumber = (value) => {
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const removeQuotes = (str) => {
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
return str.slice(1, -1);
}
return str;
};
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
if (!externalScript?.length || typeof externalScript !== 'string') {
@@ -29,8 +35,16 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
}
externalScript = externalScript?.trim();
if(scriptType === 'template-literal') {
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
const number = Number(externalScript);
// Check if the number is too high. Too high number might get altered, see #1000
if (number > Number.MAX_SAFE_INTEGER) {
return externalScript;
}
return toNumber(externalScript);
}
if (externalScript === 'true') return true;
@@ -38,6 +52,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
externalScript = removeQuotes(externalScript);
}
const vm = QuickJSSyncContext;
@@ -78,16 +94,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
}
externalScript = externalScript?.trim();
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
if (externalScript === 'true') return true;
if (externalScript === 'false') return false;
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
try {
const module = await newQuickJSWASMModule();
const vm = module.newContext();

View File

@@ -189,8 +189,8 @@ const addBruShimToContext = (vm, bru) => {
const promise = vm.newPromise();
bru.runRequest(vm.dump(args))
.then((response) => {
const { status, headers, data, dataBuffer, size } = response || {};
promise.resolve(marshallToVm(cleanJson({ status, headers, data, dataBuffer, size }), vm));
const { status, statusText, headers, data, dataBuffer, size } = response || {};
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
})
.catch((err) => {
promise.resolve(

View File

@@ -85,6 +85,14 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
return undefined;
}
if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
return templateLiteral.slice(1, -1);
}
if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
return templateLiteral.slice(1, -1);
}
if (!isNaN(templateLiteral)) {
const number = Number(templateLiteral);
// Check if the number is too high. Too high number might get altered, see #1000

View File

@@ -0,0 +1,74 @@
meta {
name: test-assert-combinations
type: http
seq: 1
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"type": "application/json",
"contentJSON": {
"string": "foo",
"stringWithSQuotes": "'foo'",
"stringWithDQuotes": "\"foo\"",
"number": 123,
"numberAsString": "123",
"numberAsStringWithSQuotes": "'123'",
"numberAsStringWithDQuotes": "\"123\"",
"numberAsStringWithLeadingZero": "0123",
"numberBig": 9007199254740992000,
"numberBigAsString": "9007199254740991999",
"null": null,
"nullAsString": "null",
"nullAsStringWithSQuotes": "'null'",
"nullAsStringWithDQuotes": "\"null\"",
"true": true,
"trueAsString": "true",
"trueAsStringWithSQuotes": "'true'",
"trueAsStringWithDQuotes": "\"true\"",
"false": false,
"falseAsString": "false",
"falseAsStringWithSQuotes": "'false'",
"falseAsStringWithDQuotes": "\"false\"",
"stringWithCurlyBraces": "{foo}",
"stringWithDoubleCurlyBraces": "{{foobar}}"
}
}
}
assert {
res.body.string: eq foo
res.body.string: eq 'foo'
res.body.string: eq "foo"
res.body.stringWithSQuotes: eq "'foo'"
res.body.stringWithDQuotes: eq '"foo"'
res.body.number: eq 123
res.body.numberAsString: eq '123'
res.body.numberAsString: eq "123"
res.body.numberAsStringWithSQuotes: eq "'123'"
res.body.numberAsStringWithDQuotes: eq '"123"'
res.body.numberAsStringWithLeadingZero: eq "0123"
res.body.numberBig.toString(): eq '9007199254740992000'
res.body.numberBigAsString: eq "9007199254740991999"
res.body.null: eq null
res.body.nullAsString: eq "null"
res.body.nullAsStringWithSQuotes: eq "'null'"
res.body.nullAsStringWithDQuotes: eq '"null"'
res.body.true: eq true
res.body.trueAsString: eq "true"
res.body.trueAsStringWithSQuotes: eq "'true'"
res.body.trueAsStringWithDQuotes: eq '"true"'
res.body.false: eq false
res.body.falseAsString: eq "false"
res.body.falseAsStringWithSQuotes: eq "'false'"
res.body.falseAsStringWithDQuotes: eq '"false"'
res.body.nonexistent: eq undefined
res.body.stringWithCurlyBraces: eq "{foo}"
res.body.stringWithDoubleCurlyBraces: eq "{{foobar}}"
}

View File

@@ -0,0 +1,22 @@
meta {
name: test echo any
type: http
seq: 11
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain" },
"content": "hello"
}
}
assert {
res.body: eq hello
}

View File

@@ -0,0 +1,22 @@
meta {
name: test echo-any json
type: http
seq: 12
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"type": "application/json",
"contentJSON": {"x": 42}
}
}
assert {
res.body.x: eq 42
}

View File

@@ -1,7 +1,14 @@
vars {
host: http://localhost:8080
httpfaker: https://www.httpfaker.org
bearer_auth_token: your_secret_token
basic_auth_password: della
env.var1: envVar1
env-var2: envVar2
bark: {{process.env.PROC_ENV_VAR}}
foo: bar
testSetEnvVar: bruno-29653
echo-host: https://echo.usebruno.com
client_id: client_id_1
client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize

View File

@@ -1,5 +1,6 @@
vars {
host: https://testbench-sanity.usebruno.com
httpfaker: https://www.httpfaker.org
bearer_auth_token: your_secret_token
basic_auth_password: della
env.var1: envVar1

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON false response
type: http
seq: 11
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "false"
}
}
assert {
res.body: eq false
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON null response
type: http
seq: 6
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "null"
}
}
assert {
res.body: eq null
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON number response
type: http
seq: 12
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "3.1"
}
}
assert {
res.body: eq 3.1
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON response
type: http
seq: 2
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"contentJSON": { "message": "hello" }
}
}
assert {
res.body.message: eq hello
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON string response
type: http
seq: 7
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "\"ok\""
}
}
assert {
res.body: eq ok
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON string with quotes response
type: http
seq: 8
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"contentJSON": "\"ok\""
}
}
assert {
res.body: eq '"ok"'
}

View File

@@ -0,0 +1,22 @@
meta {
name: test JSON true response
type: http
seq: 10
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "true"
}
}
assert {
res.body: eq true
}

View File

@@ -0,0 +1,26 @@
meta {
name: test JSON unsafe-int response
type: http
seq: 13
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "90071992547409919876"
}
}
assert {
res.body.toString(): eq 90071992547409920000
}
docs {
Note: This test is not perfect, we should match the unparsed raw-response with the expected string version of the unsafe-integer
}

View File

@@ -0,0 +1,34 @@
meta {
name: test binary response
type: http
seq: 4
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"type": "application/octet-stream",
"contentBase64": "+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA="
}
}
tests {
test("response matches the expectation after utf-8 decoding(needs improvement)", function () {
expect(res.getStatus()).to.equal(200);
const dataBinary = Buffer.from("+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=", "base64");
expect(res.body).to.equal(dataBinary.toString("utf-8"));
});
}
docs {
Note:
This test is not perfect and needs to be improved by direclty matching expected binary data with raw-response.
Currently res.body is decoded with `utf-8` by default and looses data in the process. We need some property in `res` which gives access to raw-data/Buffer.
}

View File

@@ -0,0 +1,22 @@
meta {
name: test html response
type: http
seq: 5
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/html" },
"content": "<h1>hello</h1>"
}
}
assert {
res.body: eq <h1>hello</h1>
}

View File

@@ -0,0 +1,18 @@
meta {
name: test image response
type: http
seq: 3
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"type": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURQCqAP///59OGOoAAAABYktHRAH/Ai3eAAAAB3RJTUUH6QMHCwUNKHvFmgAAABRJREFUOMtjYBgFo2AUjIJRQE8AAAV4AAEpcbn8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAzLTA3VDExOjA1OjEzKzAwOjAwQkgGWgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMy0wN1QxMTowNToxMyswMDowMDMVvuYAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDMtMDdUMTE6MDU6MTMrMDA6MDBkAJ85AAAAAElFTkSuQmCC"
}
}

View File

@@ -0,0 +1,22 @@
meta {
name: test invalid JSON response with formatting
type: http
seq: 19
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/json" },
"content": "hello\n\tworld"
}
}
assert {
res.body: eq hello\n\tworld
}

View File

@@ -0,0 +1,22 @@
meta {
name: test plain text response with formatting
type: http
seq: 18
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain" },
"content": "hello\n\tworld"
}
}
assert {
res.body: eq hello\n\tworld
}

View File

@@ -0,0 +1,23 @@
meta {
name: test plain text response
type: http
seq: 1
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain" },
"content": "hello"
}
}
assert {
res.body: eq hello
}

View File

@@ -0,0 +1,22 @@
meta {
name: test plain text utf16 response
type: http
seq: 14
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain; charset=utf-16" },
"contentBase64": "dABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AA=="
}
}
assert {
res.body: eq "this is encoded with utf16"
}

View File

@@ -0,0 +1,22 @@
meta {
name: test plain text utf16-be with BOM response
type: http
seq: 15
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain; charset=utf-16" },
"contentBase64": "/v8AdABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AC0AYgBlACAAdwBpAHQAaAAgAEIATwBN"
}
}
assert {
res.body: eq "this is encoded with utf16-be with BOM"
}

View File

@@ -0,0 +1,22 @@
meta {
name: test plain text utf16-le with BOM response
type: http
seq: 16
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain; charset=utf-16" },
"contentBase64": "//50AGgAaQBzACAAaQBzACAAZQBuAGMAbwBkAGUAZAAgAHcAaQB0AGgAIAB1AHQAZgAxADYALQBsAGUAIAB3AGkAdABoACAAQgBPAE0A"
}
}
assert {
res.body: eq "this is encoded with utf16-le with BOM"
}

View File

@@ -0,0 +1,22 @@
meta {
name: test plain text utf8 with BOM response
type: http
seq: 17
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "text/plain; charset=utf8" },
"contentBase64": "77u/dGhpcyBpcyB1dGY4IGVuY29kZWQgd2l0aCBCT00sIHdoeSBub3Q/"
}
}
assert {
res.body: eq "this is utf8 encoded with BOM, why not?"
}

View File

@@ -0,0 +1,22 @@
meta {
name: test xml response
type: http
seq: 9
}
post {
url: {{httpfaker}}/api/echo/custom
body: json
auth: none
}
body:json {
{
"headers": { "content-type": "application/xml" },
"content": "<message>hello</message>"
}
}
assert {
res.body: eq <message>hello</message>
}

View File

@@ -48,4 +48,30 @@ router.get('/iso-enc', (req, res) => {
return res.send(Buffer.from(responseText, 'latin1'));
});
router.post("/custom", (req, res) => {
const { headers, content, contentBase64, contentJSON, type } = req.body || {};
res._headers = {};
if (type) {
res.setHeader('Content-Type', type);
}
if (headers && typeof headers === 'object') {
Object.entries(headers).forEach(([key, value]) => {
res.setHeader(key, value);
});
}
if (contentBase64) {
res.write(Buffer.from(contentBase64, 'base64'));
} else if (contentJSON !== undefined) {
res.write(JSON.stringify(contentJSON));
} else if (content !== undefined) {
res.write(content);
}
return res.end();
});
module.exports = router;

View File

@@ -10,12 +10,19 @@ const multipartRouter = require('./multipart');
const app = new express();
const port = process.env.PORT || 8080;
app.use(express.raw({type: '*/*', limit: '100mb'}));
app.use(cors());
const saveRawBody = (req, res, buf) => {
req.rawBuffer = Buffer.from(buf);
req.rawBody = buf.toString();
};
app.use(bodyParser.json({ verify: saveRawBody }));
app.use(bodyParser.urlencoded({ extended: true, verify: saveRawBody }));
app.use(bodyParser.text({ verify: saveRawBody }));
app.use(xmlParser());
app.use(bodyParser.text());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.raw({ type: '*/*', limit: '100mb', verify: saveRawBody }));
formDataParser.init(app, express);
app.use('/api/auth', authRouter);