87

I'm trying to create a static file server in nodejs more as an exercise to understand node than as a perfect server. I'm well aware of projects like Connect and node-static and fully intend to use those libraries for more production-ready code, but I also like to understand the basics of what I'm working with. With that in mind, I've coded up a small server.js:

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs');
var mimeTypes = {
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "js": "text/javascript",
    "css": "text/css"};

http.createServer(function(req, res) {
    var uri = url.parse(req.url).pathname;
    var filename = path.join(process.cwd(), uri);
    path.exists(filename, function(exists) {
        if(!exists) {
            console.log("not exists: " + filename);
            res.writeHead(200, {'Content-Type': 'text/plain'});
            res.write('404 Not Found\n');
            res.end();
        }
        var mimeType = mimeTypes[path.extname(filename).split(".")[1]];
        res.writeHead(200, mimeType);

        var fileStream = fs.createReadStream(filename);
        fileStream.pipe(res);

    }); //end path.exists
}).listen(1337);

My question is twofold

  1. Is this the "right" way to go about creating and streaming basic html etc in node or is there a better/more elegant/more robust method ?

  2. Is the .pipe() in node basically just doing the following?

.

var fileStream = fs.createReadStream(filename);
fileStream.on('data', function (data) {
    res.write(data);
});
fileStream.on('end', function() {
    res.end();
});

Thanks everyone!

5
  • 2
    I wrote a module that lets you do that without compromsing flexibility. It also automatically caches all your resources. Check it out: github.com/topcloud/cachemere Commented Nov 25, 2013 at 5:09
  • 2
    A bit funny that you choose(?) to return '404 Not Found' with HTTP status code '200 OK'. If there is no resource to be found at the URL, then the appropriate code should be 404 (and what you write in document body is usually of secondary importance). You will otherwise be confusing a lot of user agents (including web crawlers and other bots) giving them documents with no real value (which they also may cache). Commented Apr 14, 2016 at 19:17
  • 1
    Thanks. Still working nicely many years after. Commented Feb 27, 2017 at 19:36
  • 1
    Thanks! this code is working perfectly. But now use fs.exists() instead of path.exists() in above code. Cheers! and yeah! don't forget return: Commented May 30, 2017 at 18:08
  • NOTE: 1) fs.exists() is deprecated. Use fs.access() or even better as for the above use case, fs.stat(). 2) url.parse is deprecated; use the newer new URL Interface instead. Commented Sep 26, 2020 at 18:56

8 Answers 8

58

Less is more

Just go command prompt first on your project and use

$ npm install express

Then write your app.js code like so:

var express = require('express'),
app = express(),
port = process.env.PORT || 4000;

app.use(express.static(__dirname + '/public'));
app.listen(port);

You would then create a "public" folder where you place your files. I tried it the harder way first but you have to worry about mime types which is just having to map stuff which is time consuming and then worry about response types, etc. etc. etc.... no thank you.

Sign up to request clarification or add additional context in comments.

9 Comments

+1 There's a lot to be said for using tested code instead of rolling your own.
I tried looking at the documentation, but can't seem to find much, can you explain what your snippet is doing? I tried to use this particular variation and I don't know what can be replaced with what.
If you want the directory listing, simply add .use(connect.directory('public')) right after the connect.static line, replacing public, with your path. Sorry for the hijacking, but I think it clears things up for me.
You might as well 'Use jQuery'! This is not an aswer to the OP's question but a solution to a problem that doesn't even exist. OP stated that the point of this experiment was to learn Node.
There is also a lot to be said about understanding the internals and rolling your own code instead of using perfectly valid and tested code that was designed in someone elses mind according to their understanding of what good abstractions and good architecture are.
|
45
  • Your basic server looks good, except:

    There is a return statement missing.

    res.write('404 Not Found\n');
    res.end();
    return; // <- Don't forget to return here !!
    

    And:

    res.writeHead(200, mimeType);

    should be:

    res.writeHead(200, {'Content-Type':mimeType});

  • Yes pipe() does basically that, it also pauses/resumes the source stream (in case the receiver is slower). Here is the source code of the pipe() function: https://github.com/joyent/node/blob/master/lib/stream.js

5 Comments

what will happen if file name is like blah.blah.css ?
mimeType shall be blah in that case xP
Isn't that the rub though? if you write your own, you are asking for these types of bugs. Good learning excerise but I am learning to appreciate "connect" rather than rolling my own. The problem with this page is people are looking just to find out how to do a simple file server and stack overflow comes up first. This answer is right but people aren't looking for it, just a simple answer. I had to find out the simpler one myself so put it here.
+1 for not pasting a link to a solution in the form of a library but actually writing an answer the question.
@ShawnWhinnery 10 years later - Thank youn for that, lol, I was confused a bit thinking - "What? that can't be an actual answer!?" :-0
21

I like understanding what's going on under the hood as well.

I noticed a few things in your code that you probably want to clean up:

  • It crashes when filename points to a directory, because exists is true and it tries to read a file stream. I used fs.lstatSync to determine directory existence.

  • It isn't using the HTTP response codes correctly (200, 404, etc)

  • While MimeType is being determined (from the file extension), it isn't being set correctly in res.writeHead (as stewe pointed out)

  • To handle special characters, you probably want to unescape the uri

  • It blindly follows symlinks (could be a security concern)

Given this, some of the apache options (FollowSymLinks, ShowIndexes, etc) start to make more sense. I've update the code for your simple file server as follows:

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs');
var mimeTypes = {
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "js": "text/javascript",
    "css": "text/css"};
 
http.createServer(function(req, res) {
  var uri = url.parse(req.url).pathname;
  var filename = path.join(process.cwd(), unescape(uri));
  var stats;


  // Prevent path traversal attack. Example:
  // curl http://127.0.0.1:1337/../../../../../etc/passwd  --path-as-is
  if (filename.indexOf('..') !==-1 ) {
        res.writeHead(400, { 'Content-Type': 'text/plain' });
        res.end('Malicious URL detected', 'utf-8');
        return;
  }

  try {
    stats = fs.lstatSync(filename); // throws if path doesn't exist
  } catch (e) {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.write('404 Not Found\n');
    res.end();
    return;
  }

 
  if (stats.isFile()) {
    // path exists, is a file
    var mimeType = mimeTypes[path.extname(filename).split(".").reverse()[0]];
    res.writeHead(200, {'Content-Type': mimeType} );
    
    var fileStream = fs.createReadStream(filename);
    fileStream.pipe(res);
  } else if (stats.isDirectory()) {
    // path exists, is a directory
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Index of '+uri+'\n');
    res.write('TODO, show index?\n');
    res.end();
  } else {
    // Symbolic link, other?
    // TODO: follow symlinks?  security?
    res.writeHead(500, {'Content-Type': 'text/plain'});
    res.write('500 Internal server error\n');
    res.end();
  }
 
}).listen(1337);

4 Comments

can i suggest "var mimeType = mimeTypes[path.extname(filename).split(".").reverse()[0]];" instead? some filenames have more than one "." eg "my.cool.video.mp4" or "download.tar.gz"
Does this somehow stop someone from using a url like folder/../../../home/user/jackpot.privatekey? I see the join to ensure the path is downstream, but I'm wondering if using the ../../../ type of notation will get around that or not. Perhaps I'll test it myself.
It does not work. I'm not sure why, but that's nice to know.
nice, a RegEx match can also collect the extension; var mimeType = mimeTypes[path.extname(filename).match(/\.([^\.]+)$/)[1]];
4
var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
  res.writeHead(200, { 'content-type': 'text/plain' })

  fs.createReadStream(process.argv[3]).pipe(res)
})

server.listen(Number(process.argv[2]))

1 Comment

Might want to explain this a bit more.
3

How about this pattern, which avoids checking separately that the file exists

        var fileStream = fs.createReadStream(filename);
        fileStream.on('error', function (error) {
            response.writeHead(404, { "Content-Type": "text/plain"});
            response.end("file not found");
        });
        fileStream.on('open', function() {
            var mimeType = mimeTypes[path.extname(filename).split(".")[1]];
            response.writeHead(200, {'Content-Type': mimeType});
        });
        fileStream.on('end', function() {
            console.log('sent file ' + filename);
        });
        fileStream.pipe(response);

3 Comments

you forgot the mimetype in case of success. I'm using this design, but instead of immediatly piping the streams, I'm piping them in the 'open' event of the filestream : writeHead for the mimetype, then pipe. The end isn't needed : readable.pipe.
Modified as per @GeH 's comment.
Should be fileStream.on('open', ...
2

I made a httpServer function with extra features for general usage based on @Jeff Ward answer

  1. custtom dir
  2. index.html returns if req === dir

Usage:

httpServer(dir).listen(port);

https://github.com/kenokabe/ConciseStaticHttpServer

Thanks.

1 Comment

0

the st module makes serving static files easy. Here is an extract of README.md:

var mount = st({ path: __dirname + '/static', url: '/static' })
http.createServer(function(req, res) {
  var stHandled = mount(req, res);
  if (stHandled)
    return
  else
    res.end('this is not a static file')
}).listen(1338)

Comments

0

@JasonSebring answer pointed me in the right direction, however his code is outdated. Here is how you do it with the newest connect version.

var connect = require('connect'),
    serveStatic = require('serve-static'),
    serveIndex = require('serve-index');

var app = connect()
    .use(serveStatic('public'))
    .use(serveIndex('public', {'icons': true, 'view': 'details'}))
    .listen(3000);

In connect GitHub Repository there are other middlewares you can use.

2 Comments

I just used express instead for a simpler answer. The newest express version has the static baked in but not much else. Thanks!
Looking at connect documentation, it is only a wrapper for middleware. All the other interesting middleware are from express repository, so technically you could use those APIs using the express.use().

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.