# User-Agent Header

> Learn how the User-Agent header identifies the client software, browser, or application making HTTP requests. Understand user agent strings and best practices.

Source: https://howhttpworks.com/headers/user-agent
Last reviewed: 2026-10-05

> **TL;DR:** `User-Agent` is the request header where a client names its software, such as `curl/8.5.0` or a browser string. The client decides what goes in it, so it can be missing, trimmed down, or faked. Use it to debug and spot trends, not to detect features, identify users or verify crawlers.

## What is User-Agent?

[RFC 9110 §10.1.5](https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5) defines it as a sequence of product identifiers with optional comments. Clients should send one unless configured otherwise, and servers must cope when it's missing.

## How User-Agent Works

A simple request (this and the other outputs on this page are trimmed examples):

```http
GET /health HTTP/1.1
Host: example.com
User-Agent: ExampleMonitor/1.0
```

Any HTTP client can send exactly that value. The server is reading a self-description, not a credential.

## Common User-Agent Strings

Browser strings pile up compatibility tokens from each other. Seeing `Safari` doesn't mean you're talking to Safari, because Chromium-based browsers include it too. [MDN describes the parsing pitfalls](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Browser_detection_using_the_user_agent).

To see what your installed curl actually sends, ask it rather than copying a version string from a blog post:

```bash
curl -v -o /dev/null https://example.com/
```

The verbose output shows the outgoing `User-Agent` line. Skip `-v` on requests that carry credentials, since it prints those too.

| Token | Parsing gotcha |
| --- | --- |
| `Safari/` | Can appear in Chromium-derived browser strings too |
| `Chrome/` | Can appear alongside another browser's identifying token |
| `Edg/` | Identifies desktop Chromium-based Edge in its UA format |
| `Firefox/` | Product token; still does not establish feature support |
| `Mozilla/5.0` | Shared compatibility prefix, not proof of Firefox |

Treat these as clues when you're reading a captured request, not as a browser parser. Order matters: match `Chrome/` before looking for `Edg/` and you'll file Edge under Chrome. Mobile formats and reduced strings make a handful of substring checks harder to keep correct over time.

## Server-Side Detection

Reading the header for diagnostics is fine. Just handle the case where it's absent:

```javascript
app.get('/client-info', (req, res) => {
  res.json({ userAgent: req.get('User-Agent') || null })
})
```

[`req.get()`](https://expressjs.com/en/5x/api/request/#req.get) reads a request header in Express. Keep a route like this narrow so it echoes only this one value, never other headers or secrets.

For logs, record the path and the supplied value together, with an explicit `null` when the field is missing:

```javascript
app.use((req, res, next) => {
  console.log(JSON.stringify({
    method: req.method,
    path: req.path,
    userAgent: req.get('User-Agent') || null
  }))
  next()
})
```

This middleware only records what came in. Bot classification and access decisions belong somewhere else. It logs `req.path` rather than the full URL because query strings often carry sensitive values.

## Content Adaptation

When you need to know what a browser can do, test for the feature on the client instead of mapping browser names to a support table:

```javascript
if ('IntersectionObserver' in window) {
  // Install observation-based enhancements.
}
if (CSS.supports('display', 'grid')) {
  // Enable the layout that requires grid.
}
```

A feature being present doesn't mean every behavior or subfeature you rely on works, so exercise the real code path in your tests.

If your server genuinely returns different content based on this header, send `Vary: User-Agent` so a shared cache won't hand one client's variant to another. The trade-off is that the cache now splits its entries by every distinct string it sees. When you control both client and server, a narrow capability signal of your own is much easier to reason about than a full browser string. [RFC 9111 §4.1](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1) defines how `Vary` selects stored responses.

Browsers have also been reducing their strings, cutting some OS, device and version detail. [MDN's reduction guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/User-agent_reduction) lists the affected fields. Old examples showing exact phone models or patch versions no longer reflect what current browsers send.

## Security and Analytics

Anyone can put `Googlebot` in the header, so the substring tells you nothing about who's calling. [Google documents reverse/forward DNS and published IP ranges for verification](https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot). Likewise, blocking a tool by name only stops callers who don't bother changing the string.

It's also a poor user identifier. Lots of clients send the same value, and one client can change its value whenever it likes.

Claiming to be a crawler takes one command:

```bash
curl -A 'Googlebot' https://example.com/
```

To verify Google's common crawlers, run a reverse DNS lookup on the source IP, check that the hostname ends in one of Google's documented domain suffixes, then resolve that hostname forward and confirm it returns the original IP. A hostname that merely contains `googlebot` somewhere fails the suffix check. The [verification guide](https://developers.google.com/crawling/docs/crawlers-fetchers/verify-google-requests) also covers IP-range checks. Follow the documented procedure for the crawler category you care about.

## Client-Side Customization

Set an explicit identifier from the command line:

```bash
curl -A 'ExampleMonitor/1.0' https://example.com/health
```

[`-A`](https://curl.se/docs/manpage.html#--user-agent) sets the value. To test what happens with no header at all, use `curl -H 'User-Agent:'`.

In browsers, `User-Agent` isn't on the [Fetch Standard's forbidden-name list](https://fetch.spec.whatwg.org/#forbidden-request-header), yet [MDN notes Chrome still drops a supplied value from Fetch requests](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header). Check the outgoing request in DevTools to see whether your value actually went out.

## Getting User-Agent right

User-Agent Client Hints expose structured browser information in fields such as `Sec-CH-UA`. They're tidier than parsing a string, but they're still client-supplied and say nothing trustworthy about the caller. [MDN documents `Sec-CH-UA` and its limited availability](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-CH-UA). Not every browser sends them, so keep a fallback for clients that don't.

Where a browser exposes the JavaScript API, read the structured values behind a presence check:

```javascript
if (navigator.userAgentData) {
  console.log(navigator.userAgentData.brands)
  console.log(navigator.userAgentData.mobile)
} else {
  console.log('User-Agent Client Hints API unavailable')
}
```

[`navigator.userAgentData`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData) needs a secure context in browsers that support it. The `mobile` hint describes the device class. It tells you nothing about viewport width or touch support, so use media queries when the question is whether your layout fits the window.

## Testing User-Agent

In a browser, compare `navigator.userAgent` in the console with **Network → Request Headers**. Against the Express diagnostic route:

```bash
curl -A 'ExampleMonitor/1.0' http://localhost:3000/client-info
curl -H 'User-Agent:' http://localhost:3000/client-info
```

The first echoes `ExampleMonitor/1.0`. The second returns `null`.

## Common Patterns

For a client you own, send an application/version token so you can trace problems back to a release. If your API has versions, select them through an explicit part of the API contract, not by guessing from a browser string.

A Node client can do this with `fetch`. Save it as an ES module:

```javascript
const response = await fetch('https://api.example.com/report', {
  headers: { 'User-Agent': 'ReportExporter/1.0' }
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
console.log(await response.text())
```

[`fetch()` is available in Node](https://nodejs.org/api/globals.html#fetch), and this is a server-side client, not a way to change a browser's identity. Replace `ReportExporter/1.0` with your real application name and version, and keep credentials out of the value.

## Related Headers

- [Accept](https://howhttpworks.com/headers/accept)
- [Accept-Language](https://howhttpworks.com/headers/accept-language)
- [Referer](https://howhttpworks.com/headers/referer)
- [Origin](https://howhttpworks.com/headers/origin)
