203

Is this safe to use require("path").join to concatenate URLs, for example:

require("path").join("http://example.com", "ok"); 
//returns 'http://example.com/ok'

require("path").join("http://example.com/", "ok"); 
//returns 'http://example.com/ok'

If not, what way would you suggest for doing this without writing code full of ifs?

Renato Gama
  • 16,431
  • 12
  • 58
  • 92
  • 3
    See also https://github.com/joyent/node/issues/2216 – Colonel Panic Apr 30 '13 at 14:49
  • 9
    In case anyone wants to use path.join, but avoid issues on Windows: `path.posix.join('/one/two/three', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', '/four') // '/one/two/three/four` – Timothy Zorn Jan 23 '17 at 18:29
  • 8
    @TimothyZorn The problem is that it if you do something like this `path.posix.join('http://localhost:9887/one/two/three/', '/four')`, the join gets rid of one of the double slashes in `http://` – Max Alexander Sep 04 '17 at 15:47
  • Ahh, yeah - good point. In those scenarios, you'd want to do something like `'http://localhost:9887/one/two/three/'.replace(/^\/+|\/+$/, '') + '/' + '/four'.replace(/^\/+|\/+$/, '')` and you *could* do `String.prototype.trimSlashes = function() { return this.replace(/^\/+|\/+$/, ''); }` if you don't want to type the regular expression over and over again. https://stackoverflow.com/a/22387870/2537258 – Timothy Zorn Sep 04 '17 at 21:35
  • or `['http://localhost:9887/one/two/three/', '/four'].map((part) => part. replace(/^\/+|\/+$/, '')).join('/')` – Timothy Zorn Sep 04 '17 at 21:42

16 Answers16

239

No. path.join() will return incorrect values when used with URLs.

It sounds like you want new URL(). From the WHATWG URL Standard:

new URL('/one', 'http://example.com/').href    // 'http://example.com/one'
new URL('/two', 'http://example.com/one').href // 'http://example.com/two'

Note that url.resolve is now marked as deprecated in the Node docs.

As Andreas correctly points out in a comment, url.resolve (also deprecated) would only help if the problem is as simple as the example. url.parse also applies to this question because it returns consistently and predictably formatted fields via the URL object that reduces the need for "code full of ifs". However, new URL() is also the replacement for url.parse.

jameshfisher
  • 34,029
  • 31
  • 121
  • 167
Matthew Bakaitis
  • 11,600
  • 7
  • 43
  • 53
  • 1
    Although not exactly what I was looking for this also solves my problem. Thanks for helping! – Renato Gama May 01 '13 at 13:29
  • 8
    @AndreasHultgren the first comment is correct. If the example was `url.resolve('/one/two/three/', 'four')` then the output would be `'one/two/three/four'`. – tavnab Jun 18 '16 at 07:16
  • @tavnab three years later I see that the comments indeed has the expected output, but my point was mainly that url.resolve might not quite be what the op asked for. It can even be dangerous if user input is not properly sanitized (e.g.`url.resolve('/one/two/three/', '../four')`), a problem a naive `.join()` doesn't have. – Andreas Hultgren Jun 18 '16 at 10:05
  • @AndreasHultgren Totally agreed. I just wanted to make it clear that `url.resolve()` indeed doesn't have the same semantics as `.join()`, as when I first read your comment I misinterpreted it as, "`url.resolve('/one/two/three', 'four')` should be expected to output `'/one/two/three/four'`". – tavnab Jun 18 '16 at 13:07
  • 10
    In case anyone wants to use path.join, but avoid issues on Windows: `path.posix.join('/one/two/three', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', '/four') // '/one/two/three/four` – Timothy Zorn Jan 23 '17 at 18:29
  • 2
    The comments are incorrect, `url.resolve('/one/two/three', 'four') // '/one/two/four'` in the answer is correct – Jonathan. Jul 30 '19 at 23:36
  • 3
    Also be aware `url.resolve()` only takes 2 arguments, where as `path.join()` takes any number. So depending on what you're doing you may need to nest calls, eg.. `url.resolve(url.resolve(SERVER_URL, pagePath), queryString)` – Molomby Aug 23 '19 at 00:31
  • 1
    Basically, `url.resolve` is resolving a relative URL (2nd param) based on an absolute URL (first param). Very useful but also very different from what `path.join` gives us. – Stijn de Witt May 13 '20 at 07:56
  • 1
    This answer should be updated to reflect the new WHATWG URL api (the legacy url API is deprecated): `const url = new URL('/one/two/three', 'https://example.com')` – Ben Davis Sep 01 '20 at 20:22
  • In what use case does `path.join()` return incorrect values when joining url parts? The examples of the op look fine. Can you please give an example? – RamboNo5 Jan 14 '21 at 11:13
59

No, you should not use path.join() to join URL elements.

There's a package for doing that now. So rather than reinvent the wheel, write all your own tests, find bugs, fix them, write more tests, find an edge case where it doesn't work, etc., you could use this package.

url-join

https://github.com/jfromaniello/url-join

Install

npm install url-join

Usage

var urljoin = require('url-join');

var fullUrl = urljoin('http://www.google.com', 'a', '/b/cd', '?foo=123');

console.log(fullUrl);

Prints:

'http://www.google.com/a/b/cd?foo=123'

Cheeso
  • 189,189
  • 101
  • 473
  • 713
stone
  • 8,422
  • 5
  • 54
  • 66
38

This can be accomplished by a combination of Node's path and URL:

  1. Require the packages:
const nodeUrl = require('url')
const nodePath = require('path')
  1. Start by making a URL object to work with:
> const myUrl = new nodeUrl.URL('https://example.com')
  1. Use pathname= and path.join to construct any possible combination:
> myUrl.pathname = nodePath.join('/search', 'for', '/something/')
'/search/for/something/'

(you can see how liberal path.join is with arguments)

  1. At this point your URL reflects the ultimate desired result:
> myUrl.toString()
'https://example.com/search/for/something/'

Why this approach?

This technique uses built-in libraries. The less third-party dependencies the better, when it comes to CVEs, maintenance, etc.

Nothing will be more proven or better tested than standard libs.

PS: Never manipulate URLs as strings!

When I review code I'm adamant about never manipulating URLs as strings manually. For one, look how complicated the spec is.

Secondly, the absence/presence of a trailing/prefixed slash (/) should not cause everything to break! You should never do:

const url = `${baseUrl}/${somePath}`

and especially not:

uri: host + '/' + SAT_SERVICE + '/' + CONSTELLATION + '/',

Of which I have seen.

Sean Patrick Murphy
  • 2,691
  • 27
  • 17
  • 1
    this should be higher up - reliable url construction with only built in packages – Josh Smith Jan 13 '22 at 23:04
  • 1
    It's better to return myUrl.href instead of calling toString() – Hoppe Jul 08 '22 at 14:22
  • 1
    with the addition of `.href` from @Hoppe by far the best approach! – Laurenz1606 Dec 28 '22 at 13:33
  • I think it is worth noting that `path.join` in windows returns a string separated with "\" instead of "/". but when we set this string as `url.pathname`, it converts all the "\"s into "/" itself. So the final result is correct although the `path.join` output was not. – Pouria Moosavi Jun 27 '23 at 08:03
21

Axios has a helper function that can combine URLs.

function combineURLs(baseURL, relativeURL) {
  return relativeURL
    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
    : baseURL;
}

Source: https://github.com/axios/axios/blob/fe7d09bb08fa1c0e414956b7fc760c80459b0a43/lib/helpers/combineURLs.js

Ikbel
  • 7,721
  • 3
  • 34
  • 45
8

The WHATWG URL object constructor has a (input, base) version, and the input can be relative using /, ./, ../. Combine this with path.posix.join and you can do anything:

const {posix} = require ("path");
const withSlash = new URL("https://example.com:8443/something/");
new URL(posix.join("a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/something/a/b/c'
new URL(posix.join("./a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/something/a/b/c'
new URL(posix.join("/a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/a/b/c'
new URL(posix.join("../a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/a/b/c'
const noSlash = new URL("https://example.com:8443/something");
new URL(posix.join("./a", "b", "c"), noSlash).toString(); // 'https://example.com:8443/a/b/c'
Coderer
  • 25,844
  • 28
  • 99
  • 154
7

If you use Angular, you can use Location:

import { Location } from '@angular/common';
// ...
Location.joinWithSlash('beginning', 'end');

Works only on 2 arguments though, so you have to chain calls or write a helper function to do that if needed.

Qortex
  • 7,087
  • 3
  • 42
  • 59
6

No! On Windows path.join will join with backslashes. HTTP urls are always forward slashes.

How about

> ["posts", "2013"].join("/")
'posts/2013'
Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
  • Good idea, but what if the first argument already have a slash at the end? eg.: `["posts/", "2013"].join("/")`? – Renato Gama Apr 30 '13 at 13:57
  • 1
    @RenatoGama, `posts//2013` is still a valid URL. – Goodwine Apr 30 '13 at 16:05
  • 3
    ^ that will not work on all domains, even though it is a valid URI. – BingeBoy Nov 30 '15 at 00:35
  • 3
    Specifically, Node's Express does not ignore extraneous slashes for routing. – Perseids Feb 03 '16 at 13:13
  • 1
    In case anyone wants to use path.join, but avoid issues on Windows: `path.posix.join('/one/two/three', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', 'four') // '/one/two/three/four`, `path.posix.join('/one/two/three/', '/four') // '/one/two/three/four` – Timothy Zorn Jan 23 '17 at 18:30
  • @Goodwine It's valid, but pointing to a **different** location. You can also have valid URLs with 3, 4 or 5 slashes in them, but all will point to different locations. So yes it is a problem and you need to make sure you are not doubling slashes when joining. – Stijn de Witt May 13 '20 at 07:51
6

When I tried PATH for concatenating url parts I run into problems. PATH.join stripes '//' down to '/' and this way invalidates an absolute url (eg. http://... -> http:/...). For me a quick fix was:

baseurl.replace(/\/$/,"") + '/' + path.replace(/^\//,"") )

or with the solution posted by Colonel Panic:

[pathA.replace(/^\/|\/$/g,""),pathB.replace(/^\/|\/$/g,"")].join("/")
Community
  • 1
  • 1
Peter
  • 69
  • 2
4

We do it like this:

var _ = require('lodash');

function urlJoin(a, b) {
  return _.trimEnd(a, '/') + '/' + _.trimStart(b, '/');
}
Peter Dotchev
  • 2,980
  • 1
  • 25
  • 19
4

If you're using lodash, you can use this simple oneliner:

// returns part1/part2/part3
['part1/', '/part2', '/part3/'].map((s) => _.trim(s, '/')).join('/')

inspired by @Peter Dotchev's answer

M K
  • 9,138
  • 7
  • 43
  • 44
2

This is what I use:

function joinUrlElements() {
  var re1 = new RegExp('^\\/|\\/$','g'),
      elts = Array.prototype.slice.call(arguments);
  return elts.map(function(element){return element.replace(re1,""); }).join('/');
}

example:

url = joinUrlElements(config.mgmtServer, '/v1/o/', config.org, '/apps');
Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • 1
    What if i'm trying to build a root-relative URL like this: `/assets/foo`? It will result into current-path-relative URL `assets/foo`. – Andrey Mikhaylov - lolmaus May 07 '15 at 12:03
  • 1
    prepend a slash ? I mean, it's a simple check; you can add it yourself. – Cheeso May 11 '15 at 15:00
  • 4
    This is how it begins... next thing you know you've spent a cumulative 8+ hours finding edge cases that don't work and fixing them over the course of your project. – stone Sep 28 '16 at 05:12
1

There are other working answers, but I went with the following. A little path.join/URL combo.

const path = require('path');
//
const baseUrl = 'http://ejemplo.mx';
// making odd shaped path pieces to see how they're handled.
const pieces = ['way//', '//over/', 'there/'];
//
console.log(new URL(path.join(...pieces), baseUrl).href);
// http://ejemplo.mx/way/over/there/

// path.join expects strings. Just an example how to ensure your pieces are Strings.
const allString = ['down', 'yonder', 20000].map(String);
console.log(new URL(path.join(...allString), baseUrl).href);
// http://ejemplo.mx/down/yonder/20000
Neil Gaetano Lindberg
  • 2,488
  • 26
  • 23
1

By the time posting this answer url.resolve() is deprecated;

I did following to join to path in Nodejs:

const path = require('path');
const url = require('url');


let myUrl = new URL('http://ignore.com');
myUrl.pathname=path.join(firstpath, secondpath);
console.log(myUrl.pathname)

This approach logs correct url path and it works for my case.

What is your opinion about this approach?

Thanks

Tornike Shavishvili
  • 1,244
  • 4
  • 16
  • 35
0

Typescript custom solution:

export function pathJoin(parts: string[], sep: string) {
  return parts
    .map(part => {
      const part2 = part.endsWith(sep) ? part.substring(0, part.length - 1) : part;
      return part2.startsWith(sep) ? part2.substr(1) : part2;
    })
    .join(sep);
}

expect(pathJoin(['a', 'b', 'c', 'd'], '/')).toEqual('a/b/c/d');
expect(pathJoin(['a/', '/b/', 'c/', 'd'], '/')).toEqual('a/b/c/d');
expect(pathJoin(['http://abc.de', 'users/login'], '/')).toEqual('http://abc.de/users/login');
Patrick Wozniak
  • 1,492
  • 1
  • 13
  • 16
0

My solution

path.join(SERVER_URL, imageAbsolutePath).replace(':/','://');

Edit: if you want to support windows enviroments

path.join(SERVER_URL, imageAbsolutePath).replace(/\\/g,'/').replace(':/','://');

The second solution will replace all the backslashes, so url parts like querystring and hash may be altered too, but the topic is joining just the url path, so I don't consider it an issue.

Killy
  • 300
  • 5
  • 13
0

The combination of the built-in path and URL libraries provides the best solution.

The answers above, however, do not handle the case where you have a relative url (ie: "../foo") that you want to add to an existing URL (ie: "http://example.com/test/bar"). Simply doing new URL("../foo","http://example.com/test/bar").href would yield "http://example.com/foo", erroneously (for me) discarding the remainder of the original path.

A simple solution is:

var base = "http://example.com:8080/foo/bar";
var rel = "../test";
var resolved = new URL( path.resolve(new URL(base).pathname, rel ), base ).href;
// Result: http://example.com:8080/foo/test

Note: If you care about URL Search Params (which seems unlikely in this scenario), you would save each of the pieces above to discrete variables and set the new url.searchParams = oldUrl.searchParams before getting the href output.

Digicrat
  • 581
  • 5
  • 13