Trying to write the following code:
index.js
const http = require('http');
const port = 9090;
const url = require('url');
const handlers = require('./handlers.js');
http.createServer((req, res) => {
req.path = url.parse(req.url).pathname;
let handler = handlers(req.path);
handler(req, res);
}).listen(port);
handlers.js:
const fs = require('fs');
const homeHandler = require('./handlers/home-handler');
const contentHandler = require('./handlers/content-handler');
module.exports = (path) => {
switch (path) {
case '/':
return homeHandler;
case '/content':
return contentHandler;
}
}
home-handler.js
const fs = require('fs')
module.exports = (req, res) => {
fs.readFile('./index.html', (err, data) => {
if (err) {
console.log(err.message);
return;
}
res.writeHead(200, {
'content-type': 'text/html'
});
res.write(data);
res.end();
return;
});
};
When I "Launch program" and run in browser localhost:9090, the function handler is executed in browser, but in the debug console it throws:
TypeError: handler is not a function
Using console.log(handler) it shows that handler is function, also instanceof shows that handler is instance of Function. What's wrong with this?
Your handlers function only handle two specific inputs, / and /content. Any other request will produce an error, since handlers will not return a function.
You might say, "That's fine! I'm not requesting a path outside of that set of two paths," but I think you might be wrong.
Most browsers will make a request for a favicon by requesting /favicon.ico in addition to making a request for the actual URL you typed in. If the browser does this additional favicon request, you will see successful results for you intended request (as you do), but then also see a failure message for the additional favicon request, which you haven't set up your code to handle.
I suggest adding a default debug handler:
module.exports = (path) => {
switch (path) {
case '/':
return homeHandler;
case '/content':
return contentHandler;
default:
(req, res) => {
res.end("Path not handled");
console.warn("No handler for", req.url);
}
}
}
const fs = require('fs')
const faviconIco = '/favicon.ico'
module.exports = (req, res) => {
fs.readFile('.' + faviconIco, (err, data) => {
if (err) {
console.log(err.message)
return
}
res.writeHead(200, {
'content-type': 'image/x-icon'
})
res.write(data)
res.end()
})
}
Related
I'm making a text hosting service and I want it to say "TextMessage Not found! You can create one in the name by clicking here" but I don't know how to do that.
I tried making a custom 404 error, and detecting if the file exists via the node fs module, that did not work out.
this is my code:
// Importing require packages/modules
const express = require("express");
const bodyParser = require("body-parser");
const fs = require("fs");
// Defining needed variables
const app = express();
const PORT = 3000;
// Express midleware to enable body-parser
app.use(bodyParser.urlencoded({ extended: false }));
// Express static handler
app.use(express.static("public"));
// POST API To create message/file
app.post("/api/message/submit", (req, res) => {
const file = req.body.messageText;
const fileID = req.body.messageID;
fs.writeFile(__dirname + "/messageFiles/" + fileID + ".txt",
file, (err) => {
if (err) res.send("ERROR! <br>" + err);
else res.send("Saved");
});
});
// GET API To read message/file
app.get("/message/:id", (req, res) => {
const msg = req.params.id;
if (fs.existsSync(__dirname + "/messageFile/" + msg + ".txt")) {
res.sendFile(__dirname + "/messageFiles/" + msg + ".txt");
} else {
res.send("Message does not exist");
}
});
// Running the server
app.listen(PORT, () => {
console.log("Running on port: " + PORT);
});
`
You may try async writing to a file by wrapping it with trycatch block. It seems you have a problem with path to the file.
I suggest you to have a helper functions to write to a file and read from a file:
const create = async (data, fileName) => {
try {
const text = await fs.writeFile(`./files/${fileName}.txt`, data);
console.log('File write successful!');
} catch (error) {
throw new Error('File write operation failed');
}
};
const read = async (fileName) => {
try {
const fileContent = await fs.readFile(`./files/${fileName}.txt`, {encoding: 'utf-8'});
return fileContent;
} catch (error) {
throw new Error('File read operation failed');
}
};
app.post("/api/message/submit", async (req, res) => {
const fileContent = req.body.messageText;
const fileID = req.body.messageID;
const message = await create(fileContent, fileID);
message ? res.send('Successful') : res.send('Failed');
});
app.get("/message/:id", async (req, res) => {
const msgID = req.params.id;
const message = await read(msgID);
res.send(message);
});
I hope it helps!
P.S.: Don't use too many comments. Your code should be self-documenting (readable)!
P.S.S: Added async keyword before (req, res).
I'm using NodeJS w/ Express to create a web app that records your audio using the VMSG library and posts the BLOB audio to my file system using HTTP Requests and multer. It also adds that instance of a recording into a MongoDB database.
I'm having an issue with the fetch command. It's not working unless I put an alert right after the fetch. The way I have it set up is that I have my main express app (index.js), and a router to the /recordingsDirectory (recordings.js) which is the endpoint for processing the posts. My main index HTML page uses Handlebars and uses a separate JS script (recorder.js) to 1) use the VMSG library and 2) fetch a POST to the /recordingsDirectory once someone submits the audio file w/ the name and the AudioBlob present. This is where I'm stuck. I can fetch in recorder.js with an alert line after the fetch, but I can't have the fetch on the end of the else if block by itself. I'd like to do it without this since the alert is ugly. A solution I've tried is that I tried to make the onsubmit function async and await fetch since I thought maybe it's waiting for a promise but that didn't work.
Here are the files. I commented CRITICAL and SUPER CRITICAL to the lines of code that you should check out and I think where the issues lie:
index.js
const express = require('express')
const handlebars = require('express-handlebars')
const path = require('path')
const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest
const xhr = new XMLHttpRequest()
const db = require('./db')
const app = express()
const PORT = process.env.PORT || 8000
app.set('view engine', 'hbs')
app.engine('hbs', handlebars({
layoutsDir: path.join(__dirname, 'views', 'layouts'),
extname: 'hbs',
defaultLayout: 'index',
partialsDir: path.join(__dirname, 'views', 'partials'),
}))
app.use(express.json())
app.use(express.urlencoded({extended: false}))
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
return res.status(400).send({ status: 404, message: err.message })
}
next()
})
app.get('/', (req, res) => {
res.render('main', {
title: 'Main Page'
})
})
app.get('/recordings', (req, res) => {
var database = db.get().db('AudioJungle')
database.collection('recordings').find().sort({ "date": -1 }).toArray(function(err, docs) {
res.render('recordings', {
title: 'Recordings',
recordings: docs
})
})
})
// CRITICAL
app.use('/recordingsDirectory', require('./recordings/recordings'))
app.use(express.static('public'))
app.use('/scripts', express.static(path.join(__dirname, 'node_modules', 'vmsg')))
db.connect(function(err) {
if (err) {
console.log('Unable to connect to Mongo.')
process.exit(1)
} else {
app.listen(PORT, () => console.log(`Listening on Port: ${PORT}`))
}
})
process.on('SIGINT', function() {
db.close(function () {
console.log('Disconnected on app termination');
process.exit(0);
});
});
app.use((req, res, next) => {
res.status(404).send({
status: 404,
error: 'Not found'
})
})
recordings.js (Aka the /recordingsDirectory endpoint for a fetch POST)
const express = require('express')
const router = express.Router()
const multer = require('multer')
const fs = require('fs-extra')
const db = require('../db')
const { ObjectId } = require('bson')
const moment = require('moment')
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, callback) => {
let path = './public/uploads'
fs.mkdirsSync(path)
callback(null, path)
},
filename: (req, file, callback) => {
createRecording(req).then((id) => {
var file_name = id + '.mp3'
callback(null, file_name)
})
}
})
})
var type = upload.single('audio-file')
// CRITICAL
router.post('/', type, (req, res) => {
console.log('made it')
res.status(200)
res.send('OK')
})
router.delete('/delete', (req, res) => {
deleteRecording(req.body._id).then((dbResponse) => {
if (dbResponse == null || dbResponse == undefined) {
res.status(400).json({ msg: 'ID already deleted' })
} else {
res.status(200)
}
})
})
router.get('/', (req, res) => {
var database = db.get().db('AudioJungle')
var recordings = database.collection('recordings')
recordings.findOne({"_id": ObjectId(req.query.id)}, function(err, result) {
if (err) throw err
if (result == null || result == undefined) {
return res.status(400).json({
status: 404,
error: 'Recording no longer in the database'
})
}
res.status(200)
res.json({
name: result.name,
date: result.date
})
})
})
async function createRecording(req) {
var database = db.get().db('AudioJungle')
var recordings = database.collection('recordings')
var audioObject = {
name: req.body.name,
date: moment().format('MMMM Do YYYY, h:mm:ss a')
}
var dbResponse = await recordings.insertOne(audioObject)
return dbResponse.insertedId
}
async function deleteRecording(id) {
var database = db.get().db('AudioJungle')
var recordings = database.collection('recordings')
var audioToDelete = {
_id: ObjectId(id)
}
var deleteResult = await recordings.deleteOne(audioToDelete)
return deleteResult
}
module.exports = router
And below is the Script the audio and name and tries to Fetch (where I need the alert for it to actually process into the /recordingsdirectory)
recorder.js
import { record } from "/scripts/vmsg.js";
let recordButton = document.getElementById("record");
var blobObj = null
recordButton.onclick = function() {
record({wasmURL: "/scripts/vmsg.wasm"}).then(blob => {
blobObj = blob
var tag = document.createElement("p")
tag.id="finishedRecording"
var text = document.createTextNode("Audio File Recorded")
tag.appendChild(text)
var element = document.getElementById("box")
element.appendChild(tag)
document.getElementById('box').appendChild(a)
})
}
let form = document.getElementById('mp3Form');
form.addEventListener("submit", submitAudio)
function submitAudio() {
var fileName = form.elements[0].value
if (fileName == "") {
alert('Please enter a name for your file')
} else if (blobObj != null) {
// CRITICAL
// SUPER CRITICAL WHERE FETCH DOESN'T WORK UNLESS I PUT AN ALERT AT THE END
const formData = new FormData()
formData.append('name', fileName)
formData.append('audio-file', blobObj)
const options = {
method: 'POST',
body: formData
}
fetch('/recordingsDirectory', options);
// If I comment an alert here, /recordingsDirectory will process the post since it console.logs 'made it'
} else {
alert('Record some Audio to upload')
}
}
Here's my file system.
Also, I'd like to mention that the fetch works properly on my Windows PC without having to add the alert, but it doesn't work without the alert on my macbook. If any one figures out a fix or an error in how I'm doing things to allow this please let me know. I've been stuck on this problem for a day now. Thanks a bunch!
Given the following setup:
const express = require("express");
const app = express();
app.get("/", function(req, res, next) {
// explicitly return an error
return next("my error");
});
// made this middleware for the example,
// solution should typically also work for express default error handling
app.use(function(error, req, res, next) {
if (error) {
res.status(500).send({ error });
throw new Error(error); // <- how to test this?
}
next();
});
app.listen(8080, function() {
console.log("server running on 8080");
}); //the server object listens on port 8080
And for the test:
const request = require("supertest");
const app = require("../../app.js");
const spy = jest.spyOn(global.console, "error").mockImplementation();
it("throws an error", async done => {
const res = await request(app).get("/");
expect(res.status).toBe(500);
expect(res.error.text).toContain("my error");
expect(spy).toHaveBeenCalled(); // nothing...
done();
});
Made a Codesandbox with this example code. Not sure how to run a node test in that though.
async shouldn't be used with done, this results in test timeout in case done() cannot be reached.
First of all, error handler shouldn't re-throw an error, unless it's reusable router instance that is supposed to be augmented with another handler. If it's the last one in a row, it should catch both synchronous and asynchronous errors that can happen inside of it.
The problem is that default error handler is triggered asynchronously so it should be specifically awaited:
it("throws an error", async () => {
const spy = jest.spyOn(global.console, "error");
const res = await request(app).get("/");
expect(res.status).toBe(500);
expect(res.error.text).toContain("my error");
await new Promise(resolve = > setTimeout(resolve));
expect(spy).not.toHaveBeenCalled(); // it really shouldn't
});
A more correct way to approach this is to make sure the error is handled:
it("throws an error", async () => {
const defaultErrorHandler = jest.fn((err, req, res, next) => {});
app.use(defaultErrorHandler);
const res = await request(app).get("/");
expect(res.status).toBe(500);
expect(res.error.text).toContain("my error");
expect(defaultErrorHandler).not.toHaveBeenCalled();
});
Here's the problem.
I have a global variable(Array type) named folders
let folders = [];
I modify it inside a Callback function inside yet another callback function.
Here's how.
app.get("/", (req, res) => {
// TODO: Proceed only if the path is correct and it is a directory
fs.readdir(dir, (err, files) => {
console.log("READING:");
if (err) throw err;
files.forEach(file => {
const add = folder => folders.push(folder);
fs.lstat(path.join(dir, file), (err, stats) => {
if (err) throw err;
if (stats.isDirectory()) {
add(file);
}
console.log("INSIDE: " + folders);
});
console.log("OUTSITE: " + folders);
});
});
res.send(folders.length.toString());
});
Now the problem is, that when I read it on this line:
res.send(folders.length.toString());
The length is always 0.
And it is also 0 on console log line where I Print it with OUTSITE but it reads fine when I print it on the line where I mention it with INSIDE.
I know the problem after some search. It happens because the callback sets the variable on a later time in the event loop.(If it makes any sense, but you get the point).
I know the problem but I don't have any idea of how to solve it. I have tried various implementations including adding a global function that pushes to the array and calling it frpm inside the callback but the results are same.
Here's the full code.
const express = require("express");
const fs = require("fs");
const path = require("path");
const os = require("os");
// Initialize Express
const app = express();
// PORT on which the app process should be started
const PORT = process.env.PORT || 5100;
// Setting Up the path to Projects folder dynamically
// !Currently only works(tested) on the Darwin(MacOS) systems PS. I don't own a Windows
// TODO: Test on Windowsn and Linux
const homedir = os.homedir();
const dir = `${homedir}/Projects/`;
// TODO: Re-Write using Async/Await as it is not fully suppported as of Node version 10.0
let folders = [];
// Home Route
app.get("/", (req, res) => {
// TODO: Proceed only if the path is correct and it is a directory
fs.readdir(dir, (err, files) => {
console.log("READING:");
if (err) throw err;
files.forEach(file => {
const add = folder => folders.push(folder);
fs.lstat(path.join(dir, file), (err, stats) => {
if (err) throw err;
if (stats.isDirectory()) {
add(file);
}
console.log("INSIDE: " + folders);
});
console.log("OUTSITE: " + folders);
});
});
res.send(folders.length.toString());
});
// Start the express server
app.listen(PORT, err => {
if (err) throw err;
console.log(`Project Lister Running On PORT: ${PORT}`);
});
Any solutions?
The issue here is that fs.lstat is asynchronous.
If you use the sync version fs.lstatSync, then you can call res.send after the forEach loop.
app.get("/", (req, res) => {
// TODO: Proceed only if the path is correct and it is a directory
fs.readdir(dir, (err, files) => {
console.log("READING:");
if (err) throw err;
files.forEach(file => {
const add = folder => folders.push(folder);
try {
const stats = fs.lstatSync(path.join(dir, file))
if (stats.isDirectory()) {
add(file);
}
} catch (err) {
throw err
}
});
res.send(folders.length.toString());
})
})
Or for a non-blocking way you could use Promise.all:
app.get("/", (req, res) => {
// TODO: Proceed only if the path is correct and it is a directory
fs.readdir(dir, (err, files) => {
console.log("READING:");
if (err) throw err;
const promises = files.map(file => {
return new Promise((resolve, reject) => {
fs.lstat(path.join(dir, file), (err, stats) => {
if (err) {
reject(err);
}
if (stats.isDirectory()) {
add(file);
resolve();
}
console.log("INSIDE: " + folders);
});
});
});
Promise.all(promises, () => {
res.send(folders.length.toString());
});
});
});
So, here's the simplest solution I can find on my own!
#PeterN's answer is correct but could be hard to wrap a beginner's head around!
Here's my final code.
const express = require("express");
const fs = require("fs").promises; // !IMPORTANT Get the promises version of the File System Module
const path = require("path");
const os = require("os");
// Initialize Express
const app = express();
// PORT on which the app process should be started
const PORT = process.env.PORT || 5100;
// Setting Up the path to Projects folder dynamically
// !Currently only works(tested) on the Darwin(MacOS) systems PS. I don't own a Windows
// TODO: Test on Windows and Linux
const homedir = os.homedir();
const dir = `${homedir}/Projects/`;
// Home Route
app.get("/", async (req, res) => {
let folders = [];
// TODO: Proceed only if the path is correct and is a directory
try {
let files = await fs.readdir(dir);
for (let i = 0; i < files.length; i++) {
let file = await fs.lstat(path.join(dir, files[i]));
if (file.isDirectory()) {
folders.push(files[i]);
}
}
} catch (error) {
console.error(error);
}
res.send(folders);
});
// Start the express server
app.listen(PORT, err => {
if (err) throw err;
console.log(`Project Lister Running On PORT: ${PORT}`);
});
Take note, on the second line where I am importing the 'fs' modules, I now import it differently or rather say a different version!
I now import it as:
const fs = require("fs").promises;
The '.promises' added at the last imports the functions, methods of that module in their Promise based implementation. Thought you must note that it is stable only in version 11.x and up of NodeJs as of right now. I am using >12.x.
Now the rest of the process is rather straight forward assuming you are familiar with Async/Await and Promises. And if you're not I would highly suggest getting into it as it can save your day as it did with me.
Here's a great tutorial regarding it: Async/Await and Promise in JS
Ps. Use the for loop instead of 'array.forEach(e => //Do Something);' approach as it will again introduce the same problem as faced earlier because it is also callback-based!
Hope I helped you. Thanks!
Before I call expressApp.use(express.static(path.join(__dirname, '/../frontend/dist'))); I need to modify the html-code. What I basically need to do is inserting meta tags in two middleware functions. I figured out how to do this. But with my solution I call a middleware-functions inside another one.
app.js
let frontend = await fs
.readFileSync(path.join(__dirname, '/../frontend/dist/index.html'))
.toString('utf8');
expressApp.use((req, res, next) => {
//...
frontend = frontend.replace(
'<meta device="ABC" />',
'<head><meta device="' + deviceId + '"/>'
);
next();
});
expressApp.use((req, res, next) => {
const language = req.get('language') || 'en_GB';
logger.info('language:' + language);
this._languageModule.setLanguage(language);
frontend = this._languageModule.insertSIDs(frontend);
logger.info(frontend);
expressApp.use(express.static(path.join(__dirname, '/../frontend/dist'))); // nested middleware function
next();
});
/** set up all the express routes **/
expressApp.get('/', (req, res) => {
res.send(frontend);
});
Edit
If I don't call expressApp.use(express.static(path.join(__dirname, '/../frontend/dist'))); nested - like this:
expressApp.use((req, res, next) => {
const language = req.get('language') || 'en_GB';
logger.info('language:' + language);
this._languageModule.setLanguage(language);
frontend = this._languageModule.insertSIDs(frontend);
logger.info(frontend);
next();
});
expressApp.use(express.static(path.join(__dirname, '/../frontend/dist')));
the HTML will not be served modified.
You probably should write your own middleware that handles the modification of the files. Here's an example not tested. But it's rough. It's based on the express.static function
const fs = require("fs");
var parseUrl = require('parseurl')
app.use((req, res, next) => {
var originalUrl = parseUrl.original(req)
var path = parseUrl(req).pathname
// make sure redirect occurs at mount
if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
path = ''
}
// We only answer to GET
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next()
}
let path = path;
fs.exists(path, (exists) => {
if(!exists)
{
// file don't exists skip this middleware
return next();
}
fs.readFile(path, (err, data) => {
if (err)
{
// Can't send the file skip this middle ware
return next();
}
// Do whatever you need with the file here?
// ...
// Setup mime type of the file
res.setHeader("content-type", "text/html");
// send the client the modified html
res.send(data);
});
console.log(exists ? 'it\'s there' : 'no passwd!');
});
});
For the original source please take a look at this github page:
https://github.com/expressjs/serve-static/blob/master/index.js