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!

yojimbo87
  • 65,684
  • 25
  • 123
  • 131
slapthelownote
  • 4,249
  • 1
  • 23
  • 27
  • 2
    I wrote a module that lets you do that without compromsing flexibility. It also automatically caches all your resources. Check it out: https://github.com/topcloud/cachemere – Jon Nov 25 '13 at 05: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). – Armen Michaeli Apr 14 '16 at 19:17
  • 1
    Thanks. Still working nicely many years after. – statosdotcom Feb 27 '17 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`: – Kaushal28 May 30 '17 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. – rags2riches-prog Sep 26 '20 at 18:56

8 Answers8

56

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.

King Friday
  • 25,132
  • 12
  • 90
  • 84
  • 2
    +1 There's a lot to be said for using tested code instead of rolling your own. – jcollum Jan 21 '13 at 18:41
  • 1
    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. – onaclov2000 Jan 31 '13 at 15:58
  • To clarify, change 'public' to the path where your own folder of files is, then to view the file check http://:3000/ if you don't enter filename, you will get a Cannot Get / (I thought it would list the directory but it turns out it doesn't. – onaclov2000 Jan 31 '13 at 16:02
  • 3
    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. – onaclov2000 Jan 31 '13 at 16:03
  • 2
    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. – Shawn Whinnery Jul 28 '14 at 21:03
  • @ShawnWhinnery is correct about not answering the fine print except about using jQuery :) but most people are like me and searched on Google for a simple file server in node.js. I think its the fault of the questioner to lead people in that way given the title he put. Hence, the votes I received as more people were similar to myself. – King Friday Jul 28 '14 at 22:27
  • 1
    @JasonSebring Why `require('http')` at the second line? – Devs love ZenUML Nov 18 '14 at 05:41
  • This answer is outdated. – ffleandro Dec 05 '14 at 11:51
  • 2
    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. – Armen Michaeli Apr 14 '16 at 15:45
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

stewe
  • 41,820
  • 13
  • 79
  • 75
  • 2
    what will happen if file name is like blah.blah.css ? – ShrekOverflow Feb 14 '12 at 08:03
  • 2
    mimeType shall be blah in that case xP – ShrekOverflow Feb 14 '12 at 08:03
  • 5
    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. – King Friday Aug 22 '12 at 21:57
  • 1
    +1 for not pasting a link to a solution in the form of a library but actually writing an answer the question. – Shawn Whinnery Jul 28 '14 at 21:07
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;

  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);
Jeff Ward
  • 16,563
  • 6
  • 48
  • 57
  • 4
    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" – unsynchronized Jun 04 '14 at 12:20
  • 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. – Reynard Aug 23 '15 at 22:53
  • It does not work. I'm not sure why, but that's nice to know. – Reynard Aug 23 '15 at 23:02
  • nice, a RegEx match can also collect the extension; ```var mimeType = mimeTypes[path.extname(filename).match(/\.([^\.]+)$/)[1]];``` – John Mutuma Nov 10 '18 at 09:58
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]))
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
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);
Aniket Sinha
  • 6,001
  • 6
  • 37
  • 50
Aerik
  • 2,307
  • 1
  • 27
  • 39
  • 1
    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](http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options). – GeH Apr 09 '14 at 21:25
  • Modified as per @GeH 's comment. – Brett Zamir Aug 01 '15 at 00:35
  • Should be `fileStream.on('open', ...` – Petah Sep 17 '15 at 22:47
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.

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)
kaore
  • 1,288
  • 9
  • 14
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.

ffleandro
  • 4,039
  • 4
  • 33
  • 48
  • I just used express instead for a simpler answer. The newest express version has the static baked in but not much else. Thanks! – King Friday Dec 05 '14 at 14:07
  • 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()`. – ffleandro Dec 05 '14 at 17:27