本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Patterns1 live hunt inside

Regex for URLs: what to match and what to leave to a parser

Finding links in text and validating a URL are different jobs. Here's a pattern for each, the traps that let bad URLs through (trailing punctuation, unescaped dots, missing anchors), and when to hand the work to new URL() or urllib.

Regex Hunter logo

· 7 min read · Updated

/^https?://\S+$/

match

  • https://a.io
  • http://x.com/p
  • https://b.dev?q=1

spare

  • ftp://x.com
  • www.x.com
  • http:/x.com

"URL regex" usually means one of two things:

  1. Finding URLs inside a block of text, like a chat app that turns links into something you can click.
  2. Validating one input, like a "Website" field on a profile form.

They need different patterns, and the second one is mostly a job for a URL parser. This post covers both, plus the traps that catch people in each. Every example is live, so edit the patterns and see what changes.

Quick answerJavaScript
/^(https?)://([^/:?#\s]+)(:\d+)?(/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/i

An http or https URL with a host, then an optional port, path, query and fragment, and no spaces.

Accepts

  • https://example.com
  • http://localhost:3000/api?x=1
  • https://example.com/blog#top

Rejects

  • ftp://example.com
  • www.example.com
  • https://exa mple.com

01Finding URLs in text

The quick version is a scheme followed by anything that isn't a space:

Matches5/5 as expected
/https?://\S+/

Should match3/3

  • Docs: https://regexhunter.com/guide.
  • (see https://example.com/a)
  • <https://example.com>

Should not2/2

  • see www.example.com
  • ftp://files.example.com
Try it in the Playground
Every line matches, but look at what gets highlighted. The period, the closing parenthesis and the > all end up inside the URL.

\S+ grabs everything up to the next space, and in real writing a URL is often followed by punctuation. Link that and the reader gets a 404.

02Dropping trailing punctuation

The fix is to say what the last character can be. The body can be anything except spaces, angle brackets and quotes. The final character also can't be common punctuation:

Matches4/4 as expected
/https?://[^\s<>"]*[^\s<>".,;:!?)]/
  • Docs: https://regexhunter.com/guide.
  • (see https://example.com/a)
  • <https://example.com>
  • https://example.com/search?q=regex&page=2
Try it in the Playground
The URL can still contain dots and colons in the middle. It just can't end on one.

This works because the greedy [^\s<>"]* runs to the end of the URL, then backs off one character at a time until the final class fits. (Our post on greedy and lazy quantifiers walks through that backing off.)

There's no perfect rule here. Wikipedia links like https://en.wikipedia.org/wiki/Regex_(disambiguation) really do end in ), and this pattern trims it. Linkify libraries handle that by checking whether the parentheses are balanced, which is more logic than one regex should carry.

Notice that www.example.com isn't found, because there's no scheme. If you want bare www. links too, start the pattern with (https?://|www\.) instead.

03Validating a URL: the parts

A web URL has up to six parts. Here they are in https://regexhunter.com:443/blog/regex-urls?ref=home#top:

Part Example Required?
scheme https yes
host regexhunter.com yes
port :443 no
path /blog/regex-urls no
query ?ref=home no
fragment #top no

A pattern that checks each part's shape looks like this:

Matches9/9 as expected
/^(https?)://([^/:?#\s]+)(:\d+)?(/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/i

Should match4/4

  • https://example.com
  • HTTPS://EXAMPLE.COM
  • http://localhost:3000/api?x=1
  • https://regexhunter.com/blog/regex-urls#top

Should not5/5

  • ftp://example.com
  • https://
  • https://exa mple.com
  • https://example.com:port
  • example.com
Try it in the Playground
Scheme, host, then an optional port, path, query and fragment, each in its own group. The i flag allows HTTPS:// in capitals.

Read it part by part:

  1. ^(https?):// the scheme, http or https, and the ://
  2. ([^/:?#\s]+) the host: anything up to the next /, :, ? or #
  3. (:\d+)? an optional port, digits only
  4. (/[^?#\s]*)? an optional path, starting with /
  5. (\?[^#\s]*)? an optional query, starting with ?
  6. (#\S*)?$ an optional fragment, then the end

It's a good shape check. But it says nothing about whether the host makes sense. https://999.999.999.999 passes, and so does https://-. You could keep adding rules for hostnames, IP addresses, IPv6 brackets, usernames and passwords, and international domain names. People have, and the results run to hundreds of characters.

04Let a parser do the parsing

Every mainstream language ships a URL parser that already knows those rules. In JavaScript (browsers and Node), the URL constructor throws on anything it can't parse:

function isHttpUrl(s) {
  try {
    const u = new URL(s);
    return u.protocol === 'http:' || u.protocol === 'https:';
  } catch {
    return false;
  }
}

new URL("https://exa mple.com"), new URL("https://999.999.999.999") and new URL("example.com") all throw. The protocol check matters: new URL("javascript:alert(1)") parses just fine, and you don't want that in a link.

You also get the pieces for free: u.hostname, u.port, u.pathname, u.search, u.hash, and u.searchParams.get("x") for one query value. And the URL comes back normalized: HTTPS://Example.COM:443/a/../b becomes https://example.com/b.

In Python, urllib.parse.urlsplit splits a URL into the same parts:

from urllib.parse import urlsplit

u = urlsplit("http://localhost:3000/api?x=1#top")
(u.scheme, u.hostname, u.port, u.path, u.query, u.fragment)
# ('http', 'localhost', 3000, '/api', 'x=1', 'top')

But urlsplit is a splitter, not a validator. It rarely raises an error. urlsplit("example.com") returns an empty scheme and host with example.com as the path, and urlsplit("https://exa mple.com") happily reports exa mple.com as the host. So check u.scheme in ("http", "https") and that u.netloc isn't empty, and add your own host rules if you need them.

05Trap: the unescaped dot

In a pattern, . means "any character". Forget the backslash in a domain name and you've allowed lookalike domains:

Matches2/2 as expected
/^https://www.example.com/
  • https://www.example.com
  • https://wwwxexample.com
Try it in the Playground
Both lines match. The dots after www and example accept any character, so someone could register wwwxexample.com.

Write www\.example\.com. Every literal dot in a pattern needs a backslash.

06Trap: the missing end

This one matters for redirect and allow-list checks, like "only send people back to URLs on our own site":

Matches2/2 as expected
/^https://example\.com/
  • https://example.com/login
  • https://example.com.evil.io
Try it in the Playground
The dots are escaped and the start is anchored, but nothing says the host ends there. The second line is someone else's domain.

The host has to stop right after .com. Require one of the characters that can end it, or the end of the string:

Matches6/6 as expected
/^https://example\.com([/?#:]|$)/

Should match3/3

  • https://example.com
  • https://example.com/login
  • https://example.com?ref=mail

Should not3/3

  • https://example.com.evil.io
  • https://example.company.io
  • https://evil.io/?next=https://example.com
Try it in the Playground
After .com there must be a /, ?, #, a port's colon, or nothing at all.

Better still for security checks: parse with new URL() or urlsplit, then compare the hostname with === or ==. An exact string comparison has no edge cases to forget.

07Try one

Every monster is a web address. The innocents use the wrong scheme, a broken ://, a typo, or no scheme at all:

Live huntpar 10

Catch the http and https addresses, spare the rest.

Targets · 0/4 caught

  • https://regexhunter.comtarget
  • http://localhost:3000target
  • https://a.io/x?y=1target
  • http://example.org/target

Innocents · 0 hit

  • ftp://example.orginnocent
  • https:/broken.cominnocent
  • example.cominnocent
  • mailto:ana@site.cominnocent
  • htps://typo.ioinnocent

Type a regex — the board updates as you go.

Try it in the Playground
Like this? A new one drops every day.Play today’s hunt

08Checklist

  • To find URLs in text: https?://[^\s<>"]*[^\s<>".,;:!?)], so trailing punctuation stays out.
  • To validate one URL: use new URL() in JavaScript and check the protocol, or urlsplit in Python and check scheme and host.
  • Escape every literal dot: example\.com.
  • In allow-lists, anchor both the start and the end of the host, or better, compare parsed hostnames.
  • Add the i flag if capitals can turn up. HTTPS://EXAMPLE.COM is a valid URL.

09Keep practising