Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.2",
"axios": "^1.7.2",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
Expand All @@ -27,5 +27,8 @@
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"mime-types": "^3.0.1"
}
}
125 changes: 123 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,129 @@
'use strict';

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime-types');

function createServer() {
/* Write your code here */
// Return instance of http.Server class
const server = http.createServer(async (req, res) => {
function checkValidation(obj) {
if (typeof obj.date !== 'string' || !obj.date.trim()) {
throw new Error(`Field of date not correct: ${obj.date}`);
}

if (typeof obj.title !== 'string' || !obj.title.trim()) {
throw new Error(`Field of title not correct: ${obj.title}`);
}

const number = +obj.amount;

if (!Number.isFinite(number) || number <= 0) {
throw new Error(`Field of amount not correct: ${number}`);
}

const date = new Date(obj.date);

if (isNaN(date.getTime())) {
throw new Error(`Field of date not correct: ${obj.date}`);
}

return true;
}

if (req.method === 'POST') {
let rawBody = '';

const contentType = req.headers['content-type'];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential crash: contentType is read from req.headers['content-type'] but may be undefined. Right after that you call contentType.includes(...) (line 47) which will throw if the header is missing. Guard the value before calling string methods or provide a default string. For example, normalize safely using const contentType = req.headers['content-type'] || '' or check if (!contentType) { ... } first. This is critical because a missing header will crash the request handler and break the server.


req.on('data', (chunk) => {
rawBody = rawBody + chunk;
});

req.on('end', () => {
let data = null;

const normalizedContentType = contentType.includes(';')
? contentType.split(';')[0]
: contentType;
Comment on lines +47 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile normalization and matching: const normalizedContentType = contentType.includes(';') ? contentType.split(';')[0] : contentType; will throw when contentType is undefined (see previous comment). Also it's clearer and more robust to split and trim once, then compare using exact match or startsWith. Example approach: const normalized = contentType ? contentType.split(';')[0].trim() : '' and then check if (normalized === 'application/json') or if (normalized.startsWith('application/json')). This avoids false negatives from charset suffixes and avoids runtime errors.


if (normalizedContentType === 'application/json') {
data = JSON.parse(rawBody);
Comment on lines +51 to +52

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncaught JSON parse: data = JSON.parse(rawBody); is not wrapped in try/catch. Malformed JSON in the request body will throw and crash the handler. Wrap the parse in a try/catch and return a 400 HTML response explaining the parse error (and include the raw body or error message for debugging). Keep error responses in HTML to be consistent with the app’s requirement to return HTML pages.

}

if (normalizedContentType === 'application/x-www-form-urlencoded') {
data = Object.fromEntries(new URLSearchParams(rawBody));
}

if (data === null) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end(`Unsupported Content-Type: ${normalizedContentType}`);
Comment on lines +60 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsupported Content-Type response is sent as text/plain (res.writeHead(400, { 'Content-Type': 'text/plain' });) and the message is Unsupported Content-Type: ${normalizedContentType}. Per the task requirement the app should return HTML pages for responses; prefer Content-Type: text/html and present the error inside HTML (e.g. <pre>...</pre>). Also include the actual received header (could be undefined) in the message so it's clear what was received, e.g. received: ${String(contentType)}.


return;
}

try {
checkValidation(data);
} catch (err) {
res.writeHead(400, { 'Content-Type': 'text/html' });

res.end(
`<pre>${err.message}</pre> <pre>${JSON.stringify(
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error response always includes { error: 'Missing required fields', ... } which can be misleading because validation may fail for other reasons (e.g. invalid amount or date format). Make the error message reflect the actual validation failure (you already include err.message above; consider using that in the JSON as well).

invalid: data,
},
null,
2,
)}</pre>`,
);

return;
}

const folder = path.join(__dirname, '..', 'db');
const filePath = path.join(folder, 'expense.json');

if (!fs.existsSync(folder)) {
fs.mkdirSync(folder, { recursive: true });
}

fs.writeFileSync(filePath, JSON.stringify(data, null, 2));

res.writeHead(200, { 'Content-Type': 'text/html' });

res.end(`<pre>${JSON.stringify(data, null, 2)}</pre>`);
Comment on lines +93 to +95

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct behavior: on successful POST you set Content-Type: 'text/html' and return the pretty-printed JSON inside <pre>...</pre>. This satisfies the requirement to return an HTML page with well-formatted JSON and is implemented correctly here. Keep this as-is.

});
}

if (req.method === 'GET') {
const base = req.headers.host
? `http://${req.headers.host}`
: 'http://localhost:5701';

const normalizedUrl = new url.URL(req.url || '', base);
const origin =
path.basename(normalizedUrl.pathname.slice(1)) || 'index.html';

const mimeType = mime.lookup(origin) || 'text/plain';

const originPathName = path.join(__dirname, origin);

const fsStream = fs.createReadStream(originPathName);

fsStream.on('error', (err) => {
res.writeHead(404, { 'Content-Type': mimeType });
res.end(`Error reading file: ${String(err)}`);
});

fsStream.on('open', () => {
res.writeHead(200, { 'Content-Type': 'text/html' });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When serving static files you always set Content-Type: 'text/html'. That is fine for HTML but incorrect for other file types (CSS, JS, images). Consider detecting the MIME type by file extension (or using a small lookup) so responses have the correct Content-Type header.

fsStream.pipe(res);
});
}
});

return server;
}

module.exports = {
Expand Down
37 changes: 37 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Form data</title>
</head>
<body>
<form
style="
display: flex;
margin-left: 20px;
flex-direction: column;
gap: 10px;
width: 200px;
"
action="/order"
method="POST"
>
<label for="date">Date:</label>
<input type="date" id="date" name="date" required />
<label for="title">Title:</label>
<input type="text" name="title" id="title" required />
<label for="amount">Amount:</label>
<input
type="number"
name="amount"
id="amount"
min="1"
step="0.01"
required
/>

<button type="submit">submit</button>
</form>
</body>
</html>
Loading