request.completeUrl contains unencoded spaces

Bug Active
The request object carries eight URL members. completeUrl is meant to be the absolute form of the URL the site actually serves, but it does not slugify folder and file names - so on any page whose folder contains a space it returns a URL with literal spaces in it. This is a defect, reported and pending a fix. Until it lands, use encodedUrl or encodedFileUrl for anything that goes into markup, a header or a sitemap.
Applies to: Hash templatesJavaScriptAPI

What you'll see

A canonical tag, an og:url, a sitemap entry or a Location header comes out containing literal spaces. Browsers often paper over it, so it survives a manual click-through and fails where nothing is watching: crawlers, link checkers, social preview fetchers and anything that compares the canonical against the requested URL.

Measured on a page stored at /Test files/zz-inventory.hash:

request.completeUrl     // https://example.net/Test files/zz-inventory     <- spaces
request.encodedUrl      // https://example.net/Test-files/zz-inventory     <- slugified

The name reads like the most complete and therefore the most correct option. It is the most complete and the least suitable for output.

What's actually happening

There are eight URL members on request, and they are not eight variations on a theme. They are two axes crossed: raw or slugified, and relative or absolute.

MemberValue for /Test files/zz-inventory.hash
url, Url/Test-files/zz-inventory
urlpath/Test-files/zz-inventory
rawurl, rawUrl/Test files/zz-inventory
completeUrlhttps://example.net/Test files/zz-inventory
encodedUrl, encodedFileUrlhttps://example.net/Test-files/zz-inventory
siteUrlhttps://example.net/

The capitalised pairs are aliases, not variants: url and Url carry the same value, as do rawurl and rawUrl. Either spelling works.

The slugified forms are what the site actually serves — a folder named Test files is reachable at /Test-files/. The raw forms mirror the names on the drive, and are legitimate for matching against a drive path.

completeUrl is the exception, and it is a defect rather than a design. It is meant to be the complete form of the URL the site serves, so it should slugify the folder and file names exactly as url and encodedUrl do. It currently does not, which is why it lands in the raw column of the table above. That has been reported and a fix is pending — read this entry as a description of current behaviour, not of intended behaviour.

Note also that the file-system members keep the extension while the URL members drop it: filepath is /Test files/zz-inventory.hash, folderpath is /Test files/. Those are drive paths, not links — see Omit file extensions in links.

One related surprise sits next to this. The bare globals site, host, domain and module are all null, which sends people looking for a way to reconstruct the host by hand. There is no need: request.sitename is example.net and request.siteUrl is https://example.net/.

What to do

1. Choose by destination, not by name.

You wantUse
A link, canonical, og:url, sitemap entry or Location headerrequest.encodedUrl
A site-relative link in markuprequest.url
The host or site rootrequest.sitename, request.siteUrl
To read or write the file behind this requestrequest.filepath, request.folderpath
To compare against a stored pathPick one family and stay in it — raw or slugified, never a mix

2. Do not use completeUrl in output while the defect stands. Once it slugifies correctly it becomes interchangeable with encodedUrl here, and this precaution can be dropped.

3. Do not encode it yourself afterwards. Running completeUrl through an encoder produces %20 where the site serves a hyphen, so the result is a valid URL that resolves to nothing. The slug is a rename, not an escape. See Do not build file and image URLs by hand.

4. Do not reach for host or domain. They are null, and because an unresolved or null name renders as an empty string rather than raising, a template that uses one produces a URL missing its host with no error anywhere. See Unresolved names in hash expressions render empty.

5. Print the members rather than guessing between them. Which one you have is a one-request question — see Use a scratch hash file to test Docly functions.