Skip to content

Instantly share code, notes, and snippets.

@ObjSal
Last active February 22, 2024 13:29
Show Gist options
  • Select an option

  • Save ObjSal/8a8bbe7809553c81e0ab309b67b4dd51 to your computer and use it in GitHub Desktop.

Select an option

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
// 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>')
}
})
// 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)
}
<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>
// 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)
}
}
})
}
// 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/`);
})
}
@amo2019
Copy link
Copy Markdown

amo2019 commented Nov 23, 2020

so useful, thanks...

@tsuccar
Copy link
Copy Markdown

tsuccar commented Jun 7, 2023

very useful. thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment