From a101870cb69fc1e702ce6876d50041a27e76ab84 Mon Sep 17 00:00:00 2001 From: ratchetsniper2 Date: Sun, 27 Oct 2024 10:47:03 +0100 Subject: [PATCH 1/4] Update and rename JsonPost.js exemple --- examples/JsonPost.js | 72 -------------------------- examples/ParseJsonOrFormBody.js | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 72 deletions(-) delete mode 100644 examples/JsonPost.js create mode 100644 examples/ParseJsonOrFormBody.js diff --git a/examples/JsonPost.js b/examples/JsonPost.js deleted file mode 100644 index abaeea1b..00000000 --- a/examples/JsonPost.js +++ /dev/null @@ -1,72 +0,0 @@ -/* Simple example of getting JSON from a POST */ - -const uWS = require('../dist/uws.js'); -const port = 9001; - -const app = uWS./*SSL*/App({ - key_file_name: 'misc/key.pem', - cert_file_name: 'misc/cert.pem', - passphrase: '1234' -}).post('/*', (res, req) => { - - /* Note that you cannot read from req after returning from here */ - let url = req.getUrl(); - - /* Read the body until done or error */ - readJson(res, (obj) => { - console.log('Posted to ' + url + ': '); - console.log(obj); - - res.end('Thanks for this json!'); - }, () => { - /* Request was prematurely aborted or invalid or missing, stop reading */ - console.log('Invalid JSON or no data at all!'); - }); - -}).listen(port, (token) => { - if (token) { - console.log('Listening to port ' + port); - } else { - console.log('Failed to listen to port ' + port); - } -}); - -/* Helper function for reading a posted JSON body */ -function readJson(res, cb, err) { - let buffer; - /* Register data cb */ - res.onData((ab, isLast) => { - let chunk = Buffer.from(ab); - if (isLast) { - let json; - if (buffer) { - try { - json = JSON.parse(Buffer.concat([buffer, chunk])); - } catch (e) { - /* res.close calls onAborted */ - res.close(); - return; - } - cb(json); - } else { - try { - json = JSON.parse(chunk); - } catch (e) { - /* res.close calls onAborted */ - res.close(); - return; - } - cb(json); - } - } else { - if (buffer) { - buffer = Buffer.concat([buffer, chunk]); - } else { - buffer = Buffer.concat([chunk]); - } - } - }); - - /* Register error cb */ - res.onAborted(err); -} diff --git a/examples/ParseJsonOrFormBody.js b/examples/ParseJsonOrFormBody.js new file mode 100644 index 00000000..52b3028b --- /dev/null +++ b/examples/ParseJsonOrFormBody.js @@ -0,0 +1,89 @@ +// Simple example of parsing JSON body or URL-encoded form + +const querystring = require('node:querystring'); +const uWS = require('../dist/uws.js'); +const port = 9001; + +/** @return {Promise} */ +const parseBody = (res) => { + // Cache parse promise + if (res._parseBodyPromise) return res._parseBodyPromise; + return res._parseBodyPromise = new Promise((resolve) => { + let buffer = Buffer.alloc(0); + // Register data callback + res.onData((ab, isLast) => { + buffer = Buffer.concat([buffer, Buffer.from(ab)]); + if (isLast) resolve(buffer); + }); + }); +}; + +/** @return {Promise} */ +const parseJSONBody = async (res) => { + // Cache parsed body + if (res._parsedJSONBody) return res._parsedJSONBody; + try { return res._parsedJSONBody = JSON.parse((await parseBody(res)).toString()); } + catch { return res._parsedJSONBody = null; } +}; + +/** @return {Promise>} */ +const parseFormBody = async (res) => { + // Cache parsed body + if (res._parsedFormBody) return res._parsedFormBody; + try { return res._parsedFormBody = querystring.parse((await parseBody(res)).toString()); } + catch { return res._parsedFormBody = null; } +}; + +const app = uWS./*SSL*/App({ + key_file_name: 'misc/key.pem', + cert_file_name: 'misc/cert.pem', + passphrase: '1234' +}).get('/jsonAPI', (res, req) => { + // Attach onAborted handler because body parsing is async + res.onAborted(() => { + res.aborted = true; + }); + + parseJSONBody(res).then((object) => { + if (res.aborted) return; + if (!object) { + console.log('Invalid JSON or no data at all!'); + res.cork(() => { // Cork because async + res.writeStatus('400 Bad Request').end(); + }); + } else { + console.log('Valid JSON: '); + console.log(object); + res.cork(() => { + res.end('Thanks for this json!'); + }); + } + }); +}).post('/formPost', (res, req) => { + // Attach onAborted handler because body parsing is async + res.onAborted(() => { + res.aborted = true; + }); + + parseFormBody(res).then((form) => { + if (res.aborted) return; + if (!form || !form.myData) { + console.log('Invalid form body or no data at all!'); + res.cork(() => { // Cork because async + res.end('Invalid form body'); + }); + } else { + console.log('Valid form body: '); + console.log(form); + res.cork(() => { + res.end('Thanks for your data!'); + }); + } + }); +}).listen(port, (token) => { + if (token) { + console.log('Listening to port ' + port); + } else { + console.log('Failed to listen to port ' + port); + } +}); From bbc90cd84a128d33d6fea909af02c8d925481952 Mon Sep 17 00:00:00 2001 From: ratchetsniper2 Date: Sun, 27 Oct 2024 12:42:37 +0100 Subject: [PATCH 2/4] Delete examples/VideoStreamerSync.js --- examples/VideoStreamerSync.js | 42 ----------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 examples/VideoStreamerSync.js diff --git a/examples/VideoStreamerSync.js b/examples/VideoStreamerSync.js deleted file mode 100644 index dd5066e4..00000000 --- a/examples/VideoStreamerSync.js +++ /dev/null @@ -1,42 +0,0 @@ -/* This is an example of sync copying of large files. - * NEVER DO THIS; ONLY FOR TESTING PURPOSES. WILL CAUSE - * SEVERE BACKPRESSURE AND HORRIBLE PERFORMANCE. - * Try navigating to the adderss with Chrome and see the video - * in real time. */ - -const uWS = require('../dist/uws.js'); -const fs = require('fs'); - -const port = 9001; -const fileName = '/home/alexhultman/Downloads/Sintel.2010.720p.mkv'; -const videoFile = toArrayBuffer(fs.readFileSync(fileName)); -const totalSize = videoFile.byteLength; - -console.log('WARNING: NEVER DO LIKE THIS; WILL CAUSE HORRIBLE BACKPRESSURE!'); -console.log('Video size is: ' + totalSize + ' bytes'); - -/* Helper function converting Node.js buffer to ArrayBuffer */ -function toArrayBuffer(buffer) { - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); -} - -/* Yes, you can easily swap to SSL streaming by uncommenting here */ -const app = uWS.SSLApp({ - key_file_name: 'misc/key.pem', - cert_file_name: 'misc/cert.pem', - passphrase: '1234' -}).get('/sintel.mkv', (res, req) => { - /* Log */ - console.log("Copying Sintel video..."); - /* Copy the entire video to backpressure */ - res.end(videoFile); -}).get('/*', (res, req) => { - /* Make sure to always handle every route */ - res.end('Nothing to see here!'); -}).listen(port, (token) => { - if (token) { - console.log('Listening to port ' + port); - } else { - console.log('Failed to listen to port ' + port); - } -}); From 6f8a29e3449b774f1233dd69dc3faf0d4611871b Mon Sep 17 00:00:00 2001 From: ratchetsniper2 Date: Sun, 27 Oct 2024 13:00:39 +0100 Subject: [PATCH 3/4] Update and rename VideoStreamer.js exemple --- examples/FileStreaming.js | 102 +++++++++++++++++++++++++++++++++ examples/VideoStreamer.js | 115 -------------------------------------- 2 files changed, 102 insertions(+), 115 deletions(-) create mode 100644 examples/FileStreaming.js delete mode 100644 examples/VideoStreamer.js diff --git a/examples/FileStreaming.js b/examples/FileStreaming.js new file mode 100644 index 00000000..caa2645c --- /dev/null +++ b/examples/FileStreaming.js @@ -0,0 +1,102 @@ +// This is an example of streaming files + +const uWS = require('../dist/uws.js'); +const fs = require('fs'); + +const port = 9001; +const bigFileName = 'absolutPathTo/bigVideo.mp3'; +const bigFileSize = fs.statSync(bigFileName).size; +console.log('Video size is: '+ bigFileSize +' bytes'); + +// Stream data to res +/** @param {import('node:Stream').Readable} readStream */ +const streamData = (res, readStream, totalSize, onFinished) => { + let chunkBuffer; // Actual chunk being streamed + let totalOffset = 0; // Actual chunk offset + const sendChunkBuffer = () => { + const [ok, done] = res.tryEnd(chunkBuffer, totalSize); + if (done) { + // Streaming finished + readStream.destroy(); + onFinished(); + } else if (ok) { + // Chunk send success + totalOffset += chunkBuffer.length; + // Resume stream if it was paused + readStream.resume(); + } else { + // Chunk send failed (client backpressure) + // onWritable will be called once client ready to receive new chunk + // Pause stream + readStream.pause(); + } + return ok; + }; + + // Attach onAborted handler because streaming is async + res.onAborted(() => { + readStream.destroy(); + onFinished(); + }); + + // Register onWritable callback + // Will be called to drain client backpressure + res.onWritable((offset) => { + const offsetDiff = offset - totalOffset; + if (offsetDiff) { + // If start of the chunk was successfully sent + // We only send the missing part + chunkBuffer = chunkBuffer.subarray(offsetDiff); + totalOffset = offset; + } + // Always return if resend was successful or not + return sendChunkBuffer(); + }); + + // Register callback for stream events + readStream.on('error', (err) => { + console.log('Error reading file: '+ err); + // res.close calls onAborted callback + res.close(); + }).on('data', (newChunkBuffer) => { + chunkBuffer = newChunkBuffer; + // Cork before sending new chunk + res.cork(sendChunkBuffer); + }); +}; + +let lastStreamIndex = 0; +let openStreams = 0; + +const app = uWS./*SSL*/App({ + key_file_name: 'misc/key.pem', + cert_file_name: 'misc/cert.pem', + passphrase: '1234' +}).get('/bigFile', (res, req) => { + const streamIndex = ++ lastStreamIndex; + console.log('Stream ('+ streamIndex +') was opened, openStreams: '+ (++ openStreams)); + res.writeHeader('Content-Type', 'video/mpeg'); + // Create read stream with fs.createReadStream + streamData(res, fs.createReadStream(bigFileName), bigFileSize, () => { + // On streaming finished (success/error/onAborted) + console.log('Stream ('+ streamIndex +') was closed, openStreams: '+ (-- openStreams)); + }); + +}).get('/smallFile', (res, req) => { + // !! Use this only for small files !! + // May cause server backpressure and bad performance + // For bigger files you have to use streaming + try { + const fileBuffer = fs.readFileSync('absolutPathTo/smallData.json'); + res.writeHeader('Content-Type', 'application/json').end(fileBuffer); + } catch (err) { + console.log('Error reading file: '+ err); + res.writeStatus('500 Internal Server Error').end(); + } +}).listen(port, (token) => { + if (token) { + console.log('Listening to port ' + port); + } else { + console.log('Failed to listen to port ' + port); + } +}); diff --git a/examples/VideoStreamer.js b/examples/VideoStreamer.js deleted file mode 100644 index fd3e3c9b..00000000 --- a/examples/VideoStreamer.js +++ /dev/null @@ -1,115 +0,0 @@ -/* This is an example of async streaming of large files. - * Try navigating to the adderss with Chrome and see the video - * in real time. */ - -const uWS = require('../dist/uws.js'); -const fs = require('fs'); - -const port = 9001; -const fileName = 'C:\\Users\\Alex\\Downloads\\Sintel.2010.720p.mkv'; -const totalSize = fs.statSync(fileName).size; - -let openStreams = 0; -let streamIndex = 0; - -console.log('Video size is: ' + totalSize + ' bytes'); - -/* Helper function converting Node.js buffer to ArrayBuffer */ -function toArrayBuffer(buffer) { - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); -} - -/* Either onAborted or simply finished request */ -function onAbortedOrFinishedResponse(res, readStream) { - - if (res.id == -1) { - console.log("ERROR! onAbortedOrFinishedResponse called twice for the same res!"); - } else { - console.log('Stream was closed, openStreams: ' + --openStreams); - console.timeEnd(res.id); - readStream.destroy(); - } - - /* Mark this response already accounted for */ - res.id = -1; -} - -/* Helper function to pipe the ReadaleStream over an Http responses */ -function pipeStreamOverResponse(res, readStream, totalSize) { - /* Careful! If Node.js would emit error before the first res.tryEnd, res will hang and never time out */ - /* For this demo, I skipped checking for Node.js errors, you are free to PR fixes to this example */ - readStream.on('data', (chunk) => { - /* We only take standard V8 units of data */ - const ab = toArrayBuffer(chunk); - - /* Store where we are, globally, in our response */ - let lastOffset = res.getWriteOffset(); - - /* Streaming a chunk returns whether that chunk was sent, and if that chunk was last */ - let [ok, done] = res.tryEnd(ab, totalSize); - - /* Did we successfully send last chunk? */ - if (done) { - onAbortedOrFinishedResponse(res, readStream); - } else if (!ok) { - /* If we could not send this chunk, pause */ - readStream.pause(); - - /* Save unsent chunk for when we can send it */ - res.ab = ab; - res.abOffset = lastOffset; - - /* Register async handlers for drainage */ - res.onWritable((offset) => { - /* Here the timeout is off, we can spend as much time before calling tryEnd we want to */ - - /* On failure the timeout will start */ - let [ok, done] = res.tryEnd(res.ab.slice(offset - res.abOffset), totalSize); - if (done) { - onAbortedOrFinishedResponse(res, readStream); - } else if (ok) { - /* We sent a chunk and it was not the last one, so let's resume reading. - * Timeout is still disabled, so we can spend any amount of time waiting - * for more chunks to send. */ - readStream.resume(); - } - - /* We always have to return true/false in onWritable. - * If you did not send anything, return true for success. */ - return ok; - }); - } - - }).on('error', () => { - /* Todo: handle errors of the stream, probably good to simply close the response */ - console.log('Unhandled read error from Node.js, you need to handle this!'); - }); - - /* If you plan to asyncronously respond later on, you MUST listen to onAborted BEFORE returning */ - res.onAborted(() => { - onAbortedOrFinishedResponse(res, readStream); - }); -} - -/* Yes, you can easily swap to SSL streaming by uncommenting here */ -const app = uWS./*SSL*/App({ - key_file_name: 'misc/key.pem', - cert_file_name: 'misc/cert.pem', - passphrase: '1234' -}).get('/sintel.mkv', (res, req) => { - /* Log */ - console.time(res.id = ++streamIndex); - console.log('Stream was opened, openStreams: ' + ++openStreams); - /* Create read stream with Node.js and start streaming over Http */ - const readStream = fs.createReadStream(fileName); - pipeStreamOverResponse(res, readStream, totalSize); -}).get('/*', (res, req) => { - /* Make sure to always handle every route */ - res.end('Nothing to see here!'); -}).listen(port, (token) => { - if (token) { - console.log('Listening to port ' + port); - } else { - console.log('Failed to listen to port ' + port); - } -}); From b0cb1f20045d6e565d9a94011c8ac1881a649561 Mon Sep 17 00:00:00 2001 From: ratchetsniper2 Date: Sun, 27 Oct 2024 23:26:59 +0100 Subject: [PATCH 4/4] Update ParseJsonOrFormBody.js example --- examples/ParseJsonOrFormBody.js | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/ParseJsonOrFormBody.js b/examples/ParseJsonOrFormBody.js index 52b3028b..e6142fbd 100644 --- a/examples/ParseJsonOrFormBody.js +++ b/examples/ParseJsonOrFormBody.js @@ -4,34 +4,34 @@ const querystring = require('node:querystring'); const uWS = require('../dist/uws.js'); const port = 9001; -/** @return {Promise} */ -const parseBody = (res) => { - // Cache parse promise - if (res._parseBodyPromise) return res._parseBodyPromise; - return res._parseBodyPromise = new Promise((resolve) => { - let buffer = Buffer.alloc(0); - // Register data callback - res.onData((ab, isLast) => { - buffer = Buffer.concat([buffer, Buffer.from(ab)]); - if (isLast) resolve(buffer); - }); +// Helper function for parsing JSON body +const parseJSONBody = (res, callback) => { + let buffer = Buffer.alloc(0); + // Register data callback + res.onData((ab, isLast) => { + buffer = Buffer.concat([buffer, Buffer.from(ab)]); + if (isLast) { + let parsedJson; + try { parsedJson = JSON.parse(buffer.toString()); } + catch { parsedJson = null; } + callback(parsedJson); + } }); }; -/** @return {Promise} */ -const parseJSONBody = async (res) => { - // Cache parsed body - if (res._parsedJSONBody) return res._parsedJSONBody; - try { return res._parsedJSONBody = JSON.parse((await parseBody(res)).toString()); } - catch { return res._parsedJSONBody = null; } -}; - -/** @return {Promise>} */ -const parseFormBody = async (res) => { - // Cache parsed body - if (res._parsedFormBody) return res._parsedFormBody; - try { return res._parsedFormBody = querystring.parse((await parseBody(res)).toString()); } - catch { return res._parsedFormBody = null; } +// Helper function for parsing URL-encoded form body +const parseFormBody = (res, callback) => { + let buffer = Buffer.alloc(0); + // Register data callback + res.onData((ab, isLast) => { + buffer = Buffer.concat([buffer, Buffer.from(ab)]); + if (isLast) { + let parsedForm; + try { parsedForm = querystring.parse(buffer.toString()); } + catch { parsedForm = null; } + callback(parsedForm); + } + }); }; const app = uWS./*SSL*/App({ @@ -44,7 +44,7 @@ const app = uWS./*SSL*/App({ res.aborted = true; }); - parseJSONBody(res).then((object) => { + parseJSONBody(res, (object) => { if (res.aborted) return; if (!object) { console.log('Invalid JSON or no data at all!'); @@ -65,7 +65,7 @@ const app = uWS./*SSL*/App({ res.aborted = true; }); - parseFormBody(res).then((form) => { + parseFormBody(res, (form) => { if (res.aborted) return; if (!form || !form.myData) { console.log('Invalid form body or no data at all!');