Last active
February 22, 2024 13:29
-
-
Save ObjSal/8a8bbe7809553c81e0ab309b67b4dd51 to your computer and use it in GitHub Desktop.
Posting form data in 3 ways to a Node.js server without third-party libraries - application/json, application/x-www-form-urlencoded, and multipart/form-data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Author: Salvador Guerrero | |
'use strict' | |
const fs = require('fs') | |
// Project modules | |
const { CreateServer } = require('./server') | |
const SecurityUtils = require('./security-utils') | |
CreateServer((request, response, body) => { | |
if (request.url === '/' && request.method === 'GET') { | |
response.setHeader('Content-Type', 'text/html') | |
const stream = fs.createReadStream(`${__dirname}/index.html`) | |
stream.pipe(body) | |
} else if (request.url === '/' && request.method === 'POST') { | |
const contentLength = 90000000000 | |
SecurityUtils.readRequestDataInMemory(request, response, body, contentLength, (error, data) => { | |
if (error) { | |
console.error(error.message) | |
return | |
} | |
// No error, all client data, server side parsing was successful. | |
// | |
// Now we can do whatever we want with the data, in the below code | |
// I'm saving the uploaded file to the root of the node server and | |
// returning the parsed data as json, I'm removing the binary data | |
// from the response. | |
// | |
// In production this can redirect to another site that makes sense, | |
// in the below commented code it redirects to the home page: | |
// response.setHeader('Location', '/') | |
// response.statusCode = 301 | |
// body.end() | |
if (data.files) { | |
for (let file of data.files) { | |
const stream = fs.createWriteStream(file.filename) | |
stream.write(file.picture, 'binary') | |
stream.close() | |
file.picture = 'bin' | |
} | |
} | |
response.setHeader('Content-Type', 'text/plain') | |
body.end(JSON.stringify(data)) | |
}) | |
} else { | |
response.setHeader('Content-Type', 'text/html') | |
response.statusCode = 404 | |
body.end('<html lang="en"><body><h1>Page Doesn\'t exist<h1></body></html>') | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Author: Salvador Guerrero | |
'use strict' | |
// https://nodejs.org/api/zlib.html | |
const zlib = require('zlib') | |
const kGzip = 'gzip' | |
const kDeflate = 'deflate' | |
const kBr = 'br' | |
const kAny = '*' | |
const kIdentity = 'identity' | |
class EncoderInfo { | |
constructor(name) { | |
this.name = name | |
} | |
isIdentity() { | |
return this.name === kIdentity | |
} | |
createEncoder() { | |
switch (this.name) { | |
case kGzip: return zlib.createGzip() | |
case kDeflate: return zlib.createDeflate() | |
case kBr: return zlib.createBrotliCompress() | |
default: return null | |
} | |
} | |
} | |
class ClientEncodingInfo { | |
constructor(name, qvalue) { | |
this.name = name | |
this.qvalue = qvalue | |
} | |
} | |
exports.getSupportedEncoderInfo = function getSupportedEncoderInfo(request) { | |
// See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 | |
let acceptEncoding = request.headers['accept-encoding'] | |
let acceptEncodings = [] | |
let knownEncodings = [kGzip, kDeflate, kBr, kAny, kIdentity] | |
// If explicit is true, then it means the client sent *;q=0, meaning accept only given encodings | |
let explicit = false | |
if (!acceptEncoding || acceptEncoding.trim().length === 0) { | |
// If the Accept-Encoding field-value is empty, then only the "identity" encoding is acceptable. | |
knownEncodings = [kIdentity] | |
acceptEncodings = [new ClientEncodingInfo(kIdentity, 1)] | |
} else { | |
// NOTE: Only return 406 if the client sends 'identity;q=0' or a '*;q=0' | |
let acceptEncodingArray = acceptEncoding.split(',') | |
for (let encoding of acceptEncodingArray) { | |
encoding = encoding.trim() | |
if (/[a-z*];q=0$/.test(encoding)) { | |
// The "identity" content-coding is always acceptable, unless | |
// specifically refused because the Accept-Encoding field includes | |
// "identity;q=0", or because the field includes "*;q=0" and does | |
// not explicitly include the "identity" content-coding. | |
let split = encoding.split(';') | |
let name = split[0].trim() | |
if (name === kAny) { | |
explicit = true | |
} | |
knownEncodings.splice(knownEncodings.indexOf(name), 1) | |
} else if (/[a-z*]+;q=\d+(.\d+)*/.test(encoding)) { | |
// This string contains a qvalue. | |
let split = encoding.split(';') | |
let name = split[0].trim() | |
let value = split[1].trim() | |
value = value.split('=')[1] | |
value = parseFloat(value) | |
acceptEncodings.push(new ClientEncodingInfo(name, value)) | |
} else { | |
// No qvalue, treat it as q=1.0 | |
acceptEncodings.push(new ClientEncodingInfo(encoding.trim(), 1.0)) | |
} | |
} | |
// order by qvalue, max to min | |
acceptEncodings.sort((a, b) => { | |
return b.qvalue - a.qvalue | |
}) | |
} | |
// `acceptEncodings` is sorted by priority | |
// Pick the first known encoding. | |
let encoding = '' | |
for (let encodingInfo of acceptEncodings) { | |
if (knownEncodings.indexOf(encodingInfo.name) !== -1) { | |
encoding = encodingInfo.name | |
break | |
} | |
} | |
// If any, pick a known encoding | |
if (encoding === kAny) { | |
for (let knownEncoding of knownEncodings) { | |
if (knownEncoding === kAny) { | |
continue | |
} else { | |
encoding = knownEncoding | |
break | |
} | |
} | |
} | |
// If no known encoding was set, then use identity if not excluded | |
if (encoding.length === 0) { | |
if (!explicit && knownEncodings.indexOf(kIdentity) !== -1) { | |
encoding = kIdentity | |
} else { | |
console.error('No known encoding were found in accept-encoding, return http status code 406') | |
return null | |
} | |
} | |
return new EncoderInfo(encoding) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html lang="en"> | |
<head> | |
<title>Home</title> | |
<script> | |
'use strict' | |
function onFormSubmit(form) { | |
const username = form["username2"].value | |
const password = form["password2"].value | |
let body = JSON.stringify({ | |
username: username, | |
password: password | |
}); | |
(async () => { | |
try { | |
const response = await fetch('/', { | |
headers: { | |
'content-type': 'application/json' | |
}, | |
method: 'POST', | |
body: body | |
}) | |
const text = await response.text() | |
if (response.status !== 200) { | |
if (text && text.length > 0) { | |
console.error(text) | |
} else { | |
console.error('There was an error without description') | |
} | |
return | |
} | |
document.body.innerHTML = text | |
} catch (e) { | |
console.error(e.message) | |
} | |
})() | |
} | |
</script> | |
</head> | |
<body> | |
<h1>application/x-www-form-urlencoded</h1> | |
<form method="post"> | |
<input id="username1" type="text" name="username" placeholder="username"><br /> | |
<input id="password1" type="password" name="password" placeholder="password"><br /> | |
<input type="submit"> | |
</form> | |
<h1>application/json</h1> | |
<form action="javascript:" onsubmit="onFormSubmit(this)"> | |
<input id="username2" type="text" placeholder="username"><br /> | |
<input id="password2" type="password" placeholder="password"><br /> | |
<input type="submit"> | |
</form> | |
<h1>multipart/form-data</h1> | |
<form method="post" enctype="multipart/form-data"> | |
<input id="username3" type="text" name="username" placeholder="username"><br /> | |
<input id="password3" type="password" name="password" placeholder="password"><br /> | |
<input id="picture3" type="file" name="picture"><br /> | |
<input type="submit"> | |
</form> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Author: Salvador Guerrero | |
'use strict' | |
const querystring = require('querystring') | |
const kApplicationJSON = 'application/json' | |
const kApplicationFormUrlEncoded = 'application/x-www-form-urlencoded' | |
const kMultipartFormData = 'multipart/form-data' | |
function endRequestWithError(response, body, statusCode, message, cb) { | |
response.statusCode = statusCode | |
if (message && message.length > 0) { | |
response.setHeader('Content-Type', 'application/json') | |
body.end(JSON.stringify({message: message})) | |
if (cb) cb(new Error(message)) | |
} else { | |
body.end() | |
if (cb) cb(new Error(`Error with statusCode: ${statusCode}`)) | |
} | |
} | |
function getMatching(string, regex) { | |
// Helper function when using non-matching groups | |
const matches = string.match(regex) | |
if (!matches || matches.length < 2) { | |
return null | |
} | |
return matches[1] | |
} | |
function getBoundary(contentTypeArray) { | |
const boundaryPrefix = 'boundary=' | |
let boundary = contentTypeArray.find(item => item.startsWith(boundaryPrefix)) | |
if (!boundary) return null | |
boundary = boundary.slice(boundaryPrefix.length) | |
if (boundary) boundary = boundary.trim() | |
return boundary | |
} | |
exports.readRequestDataInMemory = (request, response, body, maxLength, callback) => { | |
const contentLength = parseInt(request.headers['content-length']) | |
if (isNaN(contentLength)) { | |
endRequestWithError(response, body, 411, 'Length required', callback) | |
return | |
} | |
// Don't need to validate while reading, V8 runtime only reads what content-length specifies. | |
if (contentLength > maxLength) { | |
endRequestWithError(response, body, 413, `Content length is greater than ${maxLength} Bytes`, callback) | |
return | |
} | |
let contentType = request.headers['content-type'] | |
const contentTypeArray = contentType.split(';').map(item => item.trim()) | |
if (contentTypeArray && contentTypeArray.length) { | |
contentType = contentTypeArray[0] | |
} | |
if (!contentType) { | |
endRequestWithError(response, body, 400, 'Content type not specified', callback) | |
return | |
} | |
if (!/((application\/(json|x-www-form-urlencoded))|multipart\/form-data)/.test(contentType)) { | |
endRequestWithError(response, body, 400, 'Content type is not supported', callback) | |
return | |
} | |
if (contentType === kMultipartFormData) { | |
// Use latin1 encoding to parse binary files correctly | |
request.setEncoding('latin1') | |
} else { | |
request.setEncoding('utf8') | |
} | |
let rawData = '' | |
request.on('data', chunk => { | |
rawData += chunk | |
}) | |
request.on('end', () => { | |
switch (contentType) { | |
case kApplicationJSON: { | |
try { | |
callback(null, JSON.parse(rawData)) | |
} catch (e) { | |
endRequestWithError(response, body, 400, 'There was an error trying to parse the data as JSON') | |
callback(e) | |
} | |
break | |
} | |
case kApplicationFormUrlEncoded: { | |
try { | |
let parsedData = querystring.decode(rawData) | |
callback(null, parsedData) | |
} catch (e) { | |
endRequestWithError(response, body, 400, 'There was an error trying to parse the form data') | |
callback(e) | |
} | |
break | |
} | |
case kMultipartFormData: { | |
const boundary = getBoundary(contentTypeArray) | |
if (!boundary) { | |
endRequestWithError(response, body, 400, 'Boundary information missing', callback) | |
return | |
} | |
let result = {} | |
const rawDataArray = rawData.split(boundary) | |
for (let item of rawDataArray) { | |
// Use non-matching groups to exclude part of the result | |
let name = getMatching(item, /(?:name=")(.+?)(?:")/) | |
if (!name || !(name = name.trim())) continue | |
let value = getMatching(item, /(?:\r\n\r\n)([\S\s]*)(?:\r\n--$)/) | |
if (!value) continue | |
let filename = getMatching(item, /(?:filename=")(.*?)(?:")/) | |
if (filename && (filename = filename.trim())) { | |
// Add the file information in a files array | |
let file = {} | |
file[name] = value | |
file['filename'] = filename | |
let contentType = getMatching(item, /(?:Content-Type:)(.*?)(?:\r\n)/) | |
if (contentType && (contentType = contentType.trim())) { | |
file['Content-Type'] = contentType | |
} | |
if (!result.files) { | |
result.files = [] | |
} | |
result.files.push(file) | |
} else { | |
// Key/Value pair | |
result[name] = value | |
} | |
} | |
callback(null, result) | |
break | |
} | |
default: { | |
callback(null, rawData) | |
} | |
} | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Author: Salvador Guerrero | |
'use strict' | |
const fs = require('fs') | |
const http = require('http') | |
const { pipeline, PassThrough } = require('stream') | |
// Project modules | |
const { getSupportedEncoderInfo } = require('./encoding-util') | |
exports.CreateServer = function CreateServer(callback) { | |
http.createServer((request, response) => { | |
let encoderInfo = getSupportedEncoderInfo(request) | |
if (!encoderInfo) { | |
// Encoded not supported by this server | |
response.statusCode = 406 | |
response.setHeader('Content-Type', 'application/json') | |
response.end(JSON.stringify({error: 'Encodings not supported'})) | |
return | |
} | |
let body = response | |
response.setHeader('Content-Encoding', encoderInfo.name) | |
// If encoding is not identity, encode the response =) | |
if (!encoderInfo.isIdentity()) { | |
const onError = (err) => { | |
if (err) { | |
// If an error occurs, there's not much we can do because | |
// the server has already sent the 200 response code and | |
// some amount of data has already been sent to the client. | |
// The best we can do is terminate the response immediately | |
// and log the error. | |
response.end() | |
console.error('An error occurred:', err) | |
} | |
} | |
body = new PassThrough() | |
pipeline(body, encoderInfo.createEncoder(), response, onError) | |
} | |
if (request.url === '/favicon.ico' && request.method === 'GET') { | |
const path = `${__dirname}/rambo.ico` | |
const contentType = 'image/vnd.microsoft.icon' | |
// Chrome & Safari have issues caching favicon's | |
response.setHeader('Content-Type', contentType) | |
fs.createReadStream(path).pipe(body) | |
} else { | |
callback(request, response, body) | |
} | |
}).listen(3000, () => { | |
console.log(`Server running at http://localhost:3000/`); | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
so useful, thanks...