# URI Helpers

This page covers this package's `Psr\Http\Message\UriInterface` implementation
and URI helper classes for classifying, composing, resolving, normalizing,
comparing, and safely modifying URIs.

Aside from the standard `Psr\Http\Message\UriInterface` implementation provided
by the `GuzzleHttp\Psr7\Uri` class, this library also provides additional static
methods for working with URIs.

## URI Types

An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or
a relative reference. An absolute URI has a scheme. A relative reference is used
to express a URI relative to another URI, the base URI. Relative references can
be divided into several forms according to
[RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2):

- network-path references, e.g. `//example.com/path`
- absolute-path references, e.g. `/path`
- relative-path references, e.g. `subpath`

The following methods can be used to identify the type of the URI.

### `GuzzleHttp\Psr7\Uri::isAbsolute`

`public static function isAbsolute(UriInterface $uri): bool`

Whether the URI is absolute, i.e. it has a scheme.

### `GuzzleHttp\Psr7\Uri::isNetworkPathReference`

`public static function isNetworkPathReference(UriInterface $uri): bool`

Whether the URI is a network-path reference. A relative reference that begins
with two slash characters is termed a network-path reference.

### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference`

`public static function isAbsolutePathReference(UriInterface $uri): bool`

Whether the URI is an absolute-path reference. A relative reference that begins
with a single slash character is termed an absolute-path reference.

### `GuzzleHttp\Psr7\Uri::isRelativePathReference`

`public static function isRelativePathReference(UriInterface $uri): bool`

Whether the URI is a relative-path reference. A relative reference that does not
begin with a slash character is termed a relative-path reference.

### `GuzzleHttp\Psr7\Uri::isSameDocumentReference`

`public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool`

Whether the URI is a same-document reference. A same-document reference refers
to a URI that is, aside from its fragment component, identical to the base URI.
When no base URI is given, only an empty URI reference (apart from its fragment)
is considered a same-document reference.

## URI Syntax Validation

`GuzzleHttp\Psr7\Rfc3986` provides static methods for validating and
canonicalizing individual URI components against the grammar defined by
[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). They operate on raw
component strings rather than on `Psr\Http\Message\UriInterface` instances.

### `GuzzleHttp\Psr7\Rfc3986::isValidScheme`

`public static function isValidScheme(string $scheme): bool`

Whether the string is a valid URI scheme. Per
[RFC 3986 Section 3.1](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1),
a scheme must start with a letter, followed by any number of letters, digits,
`+`, `-`, or `.`. The empty string is also accepted, since a URI reference may
omit the scheme.

### `GuzzleHttp\Psr7\Rfc3986::isValidHost`

`public static function isValidHost(string $host): bool`

Whether the string is a valid URI host. Per
[RFC 3986 Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2),
the host is an IP-literal, IPv4 address, or registered name. An empty host is
accepted, since the authority, and thus the host, may be empty. Bracketed values
are validated as IPv6 or IPvFuture literals; any other value is rejected if it
contains control characters, whitespace, an authority or path delimiter (`/`,
`?`, `#`, `@`, `\`), or an embedded colon denoting a port. Percent-encoding is
validated the same way: malformed sequences (a `%` not followed by two hex
digits) and percent-encoded octets that decode to one of the rejected bytes, to
a bracket (`[`, `]`), or to `%` itself are invalid, while all other
percent-encoded octets are accepted. Rejecting these percent-encoded octets is a
deliberate guzzle host policy, stricter than the RFC 3986 `reg-name` grammar,
which permits any well-formed `pct-encoded` octet; it matches the stricter
policy used throughout the library. RFC 6874 IPv6 zone identifiers (for example
`[fe80::1%25eth0]`) are not supported.

Registered names are otherwise intentionally permissive: single-label hosts such
as `localhost`, underscores, sub-delims, and raw or percent-encoded non-ASCII
(IDN) data are accepted and preserved as given, with no punycode conversion.
IDNA is treated as a client concern. Consumers that need DNS IDNs must perform
the conversion themselves, for example via Guzzle's `idn_conversion` request
option.

### `GuzzleHttp\Psr7\Rfc3986::isValidPort`

`public static function isValidPort(string $port): bool`

Whether the string is a valid port number. RFC 3986 defines the port as
`*DIGIT`, which also permits an empty port and has no upper bound; this applies
the stricter policy used throughout the library instead, accepting a non-empty
run of digits (leading zeros are accepted and normalized) that resolves to a
value in the range 0-65535.

### `GuzzleHttp\Psr7\Rfc3986::canonicalizeIpv6`

`public static function canonicalizeIpv6(string $address): string`

Returns the [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952#section-4)
canonical form of a valid IPv6 address. The address must be a valid textual
IPv6 address without brackets and without a zone identifier, such as the
inside of an IP-literal accepted by `isValidHost()`. Canonicalization
lowercases the hexadecimal fields, suppresses leading zeros, and collapses the
longest run of two or more zero fields (the leftmost on a tie) with `::`.
Embedded dotted-decimal notation follows the rendering policy of BIND-derived
`inet_ntop()` implementations and curl: exactly the IPv4-mapped
(`::ffff:0:0/96`) and deprecated IPv4-compatible (`::/96`) layouts use it,
while other embedded-IPv4 forms, including translated (NAT64) well-known
prefixes such as `64:ff9b::/96` (RFC 6052), serialize in pure hexadecimal
fields. An `\InvalidArgumentException` is thrown if the address cannot be
parsed.

Validation is strict and platform-independent: the address is checked against
the RFC 3986 `IPv6address` grammar with PHP's `FILTER_VALIDATE_IP` filter and
parsed in pure PHP, so spellings that only some platform parsers accept, such as
the zero-padded dotted octets in `::ffff:192.168.001.001`, are rejected
everywhere.

Emitting dotted-decimal notation for these two selected layouts is the
BIND-derived `inet_ntop()` and curl compatibility policy used here.
[RFC 5952 Section 5](https://datatracker.ietf.org/doc/html/rfc5952#section-5)
permits mixed notation for recognizable embedded-IPv4 prefixes but does not
limit that category to these layouts; RFC 6052, for example, defines
`64:ff9b::/96` as a Well-Known Prefix, which this implementation renders in
pure hexadecimal. The WHATWG URL Standard always emits pure hexadecimal
fields.

## URI Components

Additional methods to work with URI components.

### `GuzzleHttp\Psr7\Uri::isDefaultPort`

`public static function isDefaultPort(UriInterface $uri): bool`

Whether the URI has the default port of the current scheme.
`Psr\Http\Message\UriInterface::getPort` may return null or the standard port.
This method can be used independently of the implementation.

### `GuzzleHttp\Psr7\Uri::composeComponents`

`public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string`

Composes a URI reference string from its various components according to
[RFC 3986 Section 5.3](https://datatracker.ietf.org/doc/html/rfc3986#section-5.3).
Usually this method does not need to be called manually but instead is used
indirectly via `Psr\Http\Message\UriInterface::__toString`.

PSR-7 UriInterface treats an empty component the same as a missing component as
`getQuery()`, `getFragment()` etc. always return a string. This explains the
slight difference to RFC 3986 Section 5.3.

Another adjustment is that the authority separator is added even when the
authority is missing/empty for the "file" scheme. This is because PHP stream
functions like `file_get_contents` only work with `file:///myfile` but not with
`file:/myfile` although they are equivalent according to RFC 3986. But
`file:///` is the more common syntax for the file scheme anyway (Chrome for
example redirects to that format). The separator is omitted when such a URI has
a rootless or empty path: adding it would turn the first path segment into the
authority of the composed URI, or compose the string `file://`, which cannot be
parsed back into a URI.

### `GuzzleHttp\Psr7\Uri::fromParts`

`public static function fromParts(array $parts): UriInterface`

Creates a URI from a hash of
[`parse_url`](https://www.php.net/manual/en/function.parse-url.php) components.

### `GuzzleHttp\Psr7\Uri::withQueryValue`

`public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface`

Creates a new URI with a specific query string value. Any existing query string
values that exactly match the provided key are removed and replaced with the
given key value pair. A value of null will set the query string key without a
value, e.g. "key" instead of "key=value".

### `GuzzleHttp\Psr7\Uri::withQueryValues`

`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface`

Creates a new URI with multiple query string values. It has the same behavior as
`withQueryValue()` but for an associative array of key => value.

### `GuzzleHttp\Psr7\Uri::withoutQueryValue`

`public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface`

Creates a new URI with a specific query string value removed. Any existing query
string values that exactly match the provided key are removed.

## Cross-Origin Detection

`GuzzleHttp\Psr7\UriComparator` provides methods to determine if a modified URI
should be considered cross-origin.

### `GuzzleHttp\Psr7\UriComparator::isCrossOrigin`

`public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool`

Determines if a modified URI should be considered cross-origin with respect to
an original URI.

Two URIs are cross-origin when their scheme, host, or effective port differ.
Host comparison is case-insensitive, and bracketed IPv6 literals are
canonicalized to their RFC 5952 form from any PSR-7 implementation before
comparison, so equivalent spellings of the same address are same-origin.
IPvFuture literals and bracketed values that cannot be parsed as an IPv6
address, such as those carrying zone identifiers, still compare as
case-insensitive text. Missing ports use the default port for `http`, `https`,
`ws`, or `wss`. Other schemes do not receive implicit default ports.

This helper only compares URI origins. It does not implement redirect handling
or credential policy.

## Reference Resolution

`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the
context of a base URI according to
[RFC 3986 Section 5](https://datatracker.ietf.org/doc/html/rfc3986#section-5).
This is also what web browsers do when resolving a link in a document based on
the current request URI.

### `GuzzleHttp\Psr7\UriResolver::resolve`

`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface`

Converts the relative URI into a new URI that is resolved against the base URI.

### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`

`public static function removeDotSegments(string $path): string`

Removes dot segments from a path and returns the new path according to
[RFC 3986 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4).

Excess `..` segments above the root of an absolute path are dropped without
consuming the root, so the result can begin with `//` (e.g. `/..//a` becomes
`//a`). Such a path is not valid for a URI without an authority (RFC 3986
Section 3.3); `resolve()` and `UriNormalizer::normalize()` serialize it with a
`/.` prefix in that case, like the WHATWG URL Standard.

### `GuzzleHttp\Psr7\UriResolver::relativize`

`public static function relativize(UriInterface $base, UriInterface $target): UriInterface`

Returns the target URI as a relative reference from the base URI. This method is
the counterpart to `resolve()`:

```php
(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
```

One use case is to use the current request URI as the base URI and then generate
relative links in your documents to reduce the document size or offer
self-contained downloadable document archives.

```php
$base = new Uri('http://example.com/a/b/');
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c'));  // prints 'c'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y'));  // prints '../x/y'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
echo UriResolver::relativize($base, new Uri('http://example.org/a/b/'));   // prints '//example.org/a/b/'.
echo UriResolver::relativize($base, new Uri('http://example.com'));         // prints '//example.com'.
```

This method also accepts a target that is already relative and will try to
relativize it further. Only a relative-path reference will be returned as-is.

```php
echo UriResolver::relativize($base, new Uri('/a/b/c'));  // prints 'c' as well
```

## Normalization and Comparison

`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs
according to
[RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).

### `GuzzleHttp\Psr7\UriNormalizer::normalize`

`public static function normalize(UriInterface $uri, int $flags = self::PRESERVING_NORMALIZATIONS): UriInterface`

Returns a normalized URI. The scheme and host component are already normalized
to lowercase per PSR-7 UriInterface. This method adds additional normalizations
that can be configured with the `$flags` parameter, which is a bitmask of
normalizations to apply.

PSR-7 UriInterface cannot distinguish between an empty component and a missing
component as `getQuery()`, `getFragment()` etc. always return a string. This
means the URIs `/?#` and `/` are treated equivalent which is not necessarily
true according to RFC 3986. But that difference is highly uncommon in reality.
So this potential normalization is implied in PSR-7 as well.

The following normalizations are available:

- `UriNormalizer::PRESERVING_NORMALIZATIONS`

    Default normalizations which only include the ones that preserve semantics.

- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING`

    All letters within a percent-encoding triplet (e.g., "%3A") are
    case-insensitive, and should be capitalized. This applies to the userinfo,
    host, path, query, and fragment components. Bracketed IP-literal hosts are
    skipped as a legacy tolerance for nonstandard values other implementations
    may carry; zone-identifier text was briefly valid URI syntax under RFC 6874,
    which RFC 9844 obsoleted and reverted. The userinfo and host are only
    rewritten when the value returned by the implementation matches the
    normalized form, and a userinfo with an empty user segment is never
    rewritten. No percent-encoding normalization is applied to a component that
    contains malformed percent syntax, such as a `%` not followed by two
    hexadecimal digits.

    Example: `http://example.org/a%c2%b1b` → `http://example.org/a%C2%B1b`

- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS`

    Decodes percent-encoded octets of unreserved characters. For consistency,
    percent-encoded octets in the ranges of ALPHA (%41–%5A and %61–%7A), DIGIT
    (%30–%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E)
    should not be created by URI producers and, when found in a URI, should be
    decoded to their corresponding unreserved characters by URI normalizers.
    This applies to the userinfo, host, path, query, and fragment components.
    Since the host is case-insensitive and PSR-7 requires it to be lowercase,
    octets decoded in the host are lowercased (e.g., "%41" becomes "a").
    Bracketed IP-literal hosts are skipped as a legacy tolerance for nonstandard
    values other implementations may carry; zone-identifier text was briefly
    valid URI syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
    userinfo and host are only rewritten when the value returned by the
    implementation matches the normalized form, and a userinfo with an empty
    user segment is never rewritten. No percent-encoding normalization is
    applied to a component that contains malformed percent syntax, such as a `%`
    not followed by two hexadecimal digits.

    Example: `http://example.org/%7Eusern%61me/` → `http://example.org/~username/`

- `UriNormalizer::CONVERT_EMPTY_PATH`

    Converts the empty path to "/" for http and https URIs.

    Example: `http://example.org` → `http://example.org/`

- `UriNormalizer::REMOVE_DEFAULT_HOST`

    Removes the default host of the given URI scheme from the URI. Only the
    "file" scheme defines the default host "localhost". All of `file:/myfile`,
    `file:///myfile`, and `file://localhost/myfile` are equivalent according to
    RFC 3986.

    Example: `file://localhost/myfile` → `file:///myfile`

- `UriNormalizer::REMOVE_DEFAULT_PORT`

    Removes the default port of the given URI scheme from the URI.

    Example: `http://example.org:80/` → `http://example.org/`

- `UriNormalizer::REMOVE_DOT_SEGMENTS`

    Removes unnecessary dot-segments. Dot-segments in relative-path references
    are not removed as it would change the semantics of the URI reference.

    Example: `http://example.org/../a/b/../c/./d.html` → `http://example.org/a/c/d.html`

- `UriNormalizer::REMOVE_DUPLICATE_SLASHES`

    Paths which include two or more adjacent slashes are converted to one.
    Webservers usually ignore duplicate slashes and treat those URIs equivalent.
    But in theory those URIs do not need to be equivalent. So this normalization
    may change the semantics. Encoded slashes (%2F) are not removed.

    Example: `http://example.org//foo///bar.html` → `http://example.org/foo/bar.html`

- `UriNormalizer::SORT_QUERY_PARAMETERS`

    Sort query parameters with their values in alphabetical order. However, the
    order of parameters in a URI may be significant (this is not defined by the
    standard). So this normalization is not safe and may change the semantics of
    the URI.

    Example: `?lang=en&article=fred` → `?article=fred&lang=en`

- `UriNormalizer::CANONICALIZE_IPV6_HOST`

    Canonicalizes IPv6 hosts to their RFC 5952 form. IPv6 addresses allow
    leading zeros and multiple placements of the `::` elision, so the same
    address has many textual spellings. The canonical form is required for
    IPv6 literals in URIs by RFC 5952 Section 6 and never changes what the URI
    refers to. Native `Uri` instances already guarantee canonical output; for
    other implementations, the canonical host is requested through
    `withHost()` and the result is kept only when the returned `getHost()`
    exactly matches the requested spelling, otherwise this step leaves the URI
    unchanged while other selected normalizations still apply, and setter
    exceptions propagate.

    Example: `http://[::0:0a]/` → `http://[::a]/`

### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent`

`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS): bool`

Whether two URIs can be considered equivalent. Both URIs are normalized
automatically before comparison with the given `$normalizations` bitmask. The
method also accepts relative URI references and returns true when they are
equivalent. This of course assumes they will be resolved against the same base
URI. If this is not the case, determination of equivalence or difference of
relative references does not mean anything.

## Related

- [PSR-7 Messages](psr-7-messages.md)
- [Streams and Decorators](streams-and-decorators.md)
- [URI and MIME Helpers](uri-and-mime-helpers.md)
- [Header and Query Helpers](header-and-query-helpers.md)
