Skip to content
Open
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
116 changes: 114 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,120 @@
'use strict';

const http = require('http');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const { pipeline } = require('stream');
const multiparty = require('multiparty');

const SUPPORTED_TYPES = {
gzip: { ext: '.gzip', method: zlib.createGzip },
deflate: { ext: '.deflate', method: zlib.createDeflate },
br: { ext: '.br', method: zlib.createBrotliCompress },
};
Comment on lines +10 to +14

Choose a reason for hiding this comment

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

Incorrect extension strings — must match requirements

SUPPORTED_TYPES uses .gzip and .deflate but the task requires exact extensions .gz and .dfl (and .br for brotli). This will produce incorrect response filenames and fail tests. Update the mapping to exactly:

gzip: { ext: '.gz', method: zlib.createGzip },
deflate: { ext: '.dfl', method: zlib.createDeflate },
br: { ext: '.br', method: zlib.createBrotliCompress }

(Requirement: compression mapping: gzip → .gz, deflate → .dfl, br → .br.)


function createServer() {
/* Write your code here */
// Return instance of http.Server class
const server = new http.Server();

server.on('request', (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);

if (
req.method === 'GET' &&
(url.pathname === '/' || url.pathname === '/index.html')
) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');

res.end();

return;
Comment on lines +22 to +31

Choose a reason for hiding this comment

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

GET / and /index.html returns an empty page — serve the HTML form

You set Content-Type: text/html and immediately res.end() with no body. The task requires an HTML form for uploading. Either stream public/index.html here or remove this special-case so the static-serving code later serves index.html. As-is, GET / returns an empty body which does not meet the requirement.

}

if (url.pathname === '/compress') {
if (req.method !== 'POST') {
res.writeHead(400);
res.end('Use POST method');

return;
}

const form = new multiparty.Form();

form.parse(req, (err, fields, files) => {
Comment on lines +42 to +44

Choose a reason for hiding this comment

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

No explicit Content-Type validation for multipart/form-data

The requirements mandate returning 400 when POST /compress is not sent as multipart/form-data. Currently you call form.parse(req, ...) and rely on multiparty to fail. Add an explicit check on req.headers['content-type'] (e.g., startsWith 'multipart/form-data') and return 400 early if it doesn't match, so behavior is explicit and predictable.

if (err) {
res.writeHead(400);
res.end('Problem with form');

return;
}

const compressionType = fields.compressionType?.[0];
const file = files.file?.[0];

if (!compressionType || !file) {
res.writeHead(400);
res.end('Incorrectly filled out form');

return;
}

const compression = SUPPORTED_TYPES[compressionType];

if (!compression) {
res.writeHead(400);
res.end('Wrong compression type');

return;
}

const originalFilename = path.basename(file.originalFilename);
const compressedFilename = originalFilename + compression.ext;
Comment on lines +71 to +72

Choose a reason for hiding this comment

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

Missing validation of originalFilename before using it

You call path.basename(file.originalFilename) without checking that file.originalFilename exists. The checklist requires treating missing original filename as invalid form and responding with 400. Add an explicit check and return 400 if file.originalFilename is falsy before constructing compressedFilename.


res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${compressedFilename}`,
Comment on lines +74 to +76

Choose a reason for hiding this comment

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

Content-Disposition filename must be quoted and use correct extension

The header is set as attachment; filename=${compressedFilename} (unquoted). Filenames containing spaces or special characters require quoting and tests commonly expect a quoted filename. Also ensure compressedFilename uses the corrected extension from SUPPORTED_TYPES. Example:

'Content-Disposition': `attachment; filename="${compressedFilename}"`

Apply this change when you correct the extension mapping.

});

const source = fs.createReadStream(file.path);
const compress = compression.method();

pipeline(source, compress, res, (erro) => {
fs.unlink(file.path, () => {});

if (erro) {
res.writeHead(400);
res.end('Compression error');
}
});
Comment on lines +82 to +89

Choose a reason for hiding this comment

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

Pipeline error handling is fragile — close response when headers already sent; use 500 for server errors

In the pipeline callback you attempt to send a 400 and res.end on error. If headers were already sent this will fail and may leave the client hanging. Recommendations:

  • Use if (res.headersSent) { return res.destroy(err); } to close the connection when streaming has started.
  • Use HTTP 500 for unexpected compression/server errors rather than 400.
  • Ensure the temporary uploaded file is unlinked in all cases (success and error).
  • Add 'error' listeners on source and compress to destroy response and streams on error.
  • Rename erro to err for clarity.

Example pattern:

pipeline(source, compress, res, (err) => {
  fs.unlink(file.path, () => {});
  if (err) {
    if (res.headersSent) {
      res.destroy(err);
    } else {
      res.writeHead(500);
      res.end('Compression error');
    }
  }
});

This ensures the connection does not hang and errors are signaled with appropriate status codes.

});

return;
}

const fileName = url.pathname.slice(1) || 'index.html';
const filePath = path.resolve('public', fileName);

if (!fs.existsSync(filePath)) {
res.statusCode = 404;
res.end('file not found');

return;
}

const fileStream = fs.createReadStream(filePath);

fileStream.pipe(res);

fileStream.on('error', () => {
res.statusCode = 500;
res.end('server error');
});

res.on('close', () => fileStream.destroy());
});

return server;
}

module.exports = {
Expand Down
Loading