# HTTP GET Method: Complete Guide with Examples

> Learn how the HTTP GET method works. Understand when to use GET requests, query parameters, caching, and best practices with real-world examples.

Source: https://howhttpworks.com/methods/get
Last reviewed: 2026-10-05

> **TL;DR:** GET fetches whatever lives at a URL: a page, an image, a JSON record. It should never change server state, which is why browsers, crawlers, and caches feel free to repeat and prefetch it. Put parameters in the query string, not a body. Responses are cacheable when the status and `Cache-Control` headers allow it.

## What is GET?

A GET asks for the current version of whatever is at a URL. It shouldn't change anything. `GET /orders/42/cancel` breaks that contract even if the handler works, because crawlers and link prefetchers follow GET links freely, without asking anyone to approve a write.

Requests and responses on this page are examples:

```http
GET /products?category=books HTTP/1.1
Host: api.example.com
Accept: application/json

```

[RFC 9110 §9.3.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.1) gives a GET body no defined meaning, and the browser Fetch API refuses to send one. For a lookup, query parameters are the portable choice.

## Key Characteristics

"Safe" is about what the client asks for, not every side effect. Logging a GET or bumping a usage counter is fine. "Idempotent" is about the intended effect on server state, not identical bytes: a second GET can return a newer price or a different status.

GET responses are cacheable, though not every one gets stored. `Cache-Control: no-store` keeps a response out of caches; `no-cache` lets caches store it but makes them revalidate before each reuse. Freshness is measured against the response's age, which includes time spent in upstream caches, not just time since your browser received it. See [HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching).

## Query Parameters

Let curl encode values instead of assembling the query by hand:

```bash
curl --get 'https://api.example.com/products' \
  --data-urlencode 'q=red shoes' \
  --data-urlencode 'category=books'
```

The endpoint decides what `q` and `category` mean; HTTP has no standard names for pagination or filters. Keep credentials out of the URL. HTTPS encrypts it in transit, but URLs still land in server logs and browser history.

In browser code, build the URL with `URLSearchParams`. Use `append()` when the endpoint expects a repeated key for a multi-value filter:

```javascript
const url = new URL('/api/products', location.origin)
url.searchParams.set('q', 'red shoes')
url.searchParams.set('page', '2')
url.searchParams.append('tag', 'books')
url.searchParams.append('tag', 'sale')
console.log(url.search)
```

Pass raw values to `set()`; it does the percent-encoding, so encoding them yourself first double-encodes them. Likewise, a literal ampersand in a search term goes into the value, where it gets encoded, rather than being concatenated into the query string where it would split the parameter. The [URLSearchParams documentation](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) covers both behaviors.

## Real-World Examples

Look at the headers and body together, then repeat the request with the ETag your server actually returned:

```bash
curl -i 'https://api.example.com/products/42'
curl -i 'https://api.example.com/products/42' \
  -H 'If-None-Match: "catalog-v7"'
```

If that validator still matches, you get:

```http
HTTP/1.1 304 Not Modified
Date: Mon, 05 Oct 2026 12:00:00 GMT
ETag: "catalog-v7"
Cache-Control: max-age=60

```

A 304 has no body; the client reuses the copy it already has. When both validators are sent, `If-None-Match` takes precedence over `If-Modified-Since`.

### Serving a downloadable file

In Express, let the file-serving API send the file and its headers together. This runnable example serves one fixed file, so no request parameter can point it at an arbitrary path on disk:

```javascript
const express = require('express')
const path = require('node:path')
const app = express()
app.get('/downloads/report', (_req, res, next) => {
  res.download(path.join(__dirname, 'files/report.pdf'), 'report.pdf', (error) => {
    if (error) next(error)
  })
})
app.listen(3000)
```

Create `files/report.pdf` before testing. `res.download()` sends the file with an attachment disposition, so the browser saves it. `res.attachment('report.csv')` only sets headers, and you still have to send the content yourself, for example `res.attachment('report.csv').send('name\nAvery\n')`. Use `res.sendFile()` when you want file serving without forcing attachment. [Express documents these APIs](https://expressjs.com/en/5x/api/response/#res.download), including the need to handle errors after a transfer has partially started.

In Django, a view can return a binary file with `FileResponse`:

```python
from pathlib import Path
from django.conf import settings
from django.http import FileResponse
from django.views.decorators.http import require_safe

@require_safe
def download_report(request):
    report = Path(settings.BASE_DIR) / "files" / "report.pdf"
    return FileResponse(report.open("rb"), as_attachment=True, filename="report.pdf")
```

Map the view in your URL configuration. Open the file without a `with` block: [FileResponse closes it](https://docs.djangoproject.com/en/5.2/ref/request-response/#fileresponse-objects) once it's served, and a `with` block would close it too early. If the report is private, check authorization before opening it.

## Common Headers

`Accept` says which media type the client wants; `Content-Type` says what it got. `ETag` identifies a version for conditional requests. `Vary: Accept-Encoding` tells caches that the response depends on that request header. Bearer tokens go in `Authorization`, not a query parameter.

None of these are required on every GET; they're the ones you'll reach for when debugging:

| Header | Example value | Debugging use |
| --- | --- | --- |
| Accept | `application/json` | Compare what the client requested with the returned Content-Type. |
| Accept-Language | `en-US,en;q=0.9` | Check whether language negotiation changes the representation. |
| Accept-Encoding | `gzip, br` | Compare compressed and uncompressed variants. |
| If-None-Match | `"catalog-v7"` | Validate the representation already stored by the client. |
| Cache-Control | `no-cache` | Request validation before a stored response is reused. |

For a download, `curl -D - -o ./report.pdf URL` writes the body to disk while leaving headers visible. `curl --compressed` asks for a compressed encoding curl supports and decodes what comes back. When the bug is in the GET body, test with GET; HEAD won't show it.

## Response Status Codes

A successful retrieval commonly returns [200](https://howhttpworks.com/status-codes/200); a successful byte-range request can return [206](https://howhttpworks.com/status-codes/206). A conditional request can return [304](https://howhttpworks.com/status-codes/304). A [404](https://howhttpworks.com/status-codes/404) sometimes hides a resource the server won't admit exists, so it doesn't mean the URL never existed.

## GET vs POST

GET reads. POST asks the server to process what you send, however that resource defines it. POST can carry a large search document that won't fit comfortably in a query string, but its responses are much harder to cache. Neither method encrypts anything; that's HTTPS's job.

There's no universal 2,000-character URL limit. Each hop sets its own. For example, nginx requires a request line to fit in one `large_client_header_buffers` buffer and returns 414 otherwise; its [documented default](https://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers) is `4 8k`. Four 8k buffers still mean a single request line has to fit in 8k, not 32k.

## Getting GET right

If opening or refreshing a URL changes business state, change the handler or the method. Adding `?action=delete` to the URL doesn't make a mutating GET safe. When a response contains account data, set an explicit cache policy such as `Cache-Control: private` or `no-store` instead of hoping the URL keeps it private.

## JavaScript Examples

```javascript
const params = new URLSearchParams({ q: 'red shoes', category: 'books' })
const response = await fetch(`/api/products?${params}`)
if (!response.ok) throw new Error(`GET failed: ${response.status}`)
const products = await response.json()
```

Fetch resolves normally on HTTP errors like 404, so check `ok` before you parse the body as a success payload.

A failed request and an HTTP error are different paths. Keep the status visible in the error you report:

```javascript
async function fetchDocument(id) {
  const response = await fetch(`/api/documents/${encodeURIComponent(id)}`, {
    headers: { Accept: 'application/json' }
  })
  if (response.status === 404) throw new Error('Document not found')
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}
```

If `json()` throws, check Content-Type and the body in DevTools. A reverse proxy or CDN may return an HTML error page from a URL that normally returns JSON; your API's contract only covers the responses your app generates.

## Try It Yourself

Open the [GET playground preset](https://howhttpworks.com/tools/playground?preset=get-one), change the query, and watch the request URL change. When debugging caching on your own server, compare the first response's ETag with the validator sent on the next request.

On a conditional GET, check that the 304 comes back with no body. If your code sends its own `If-None-Match` and then calls `json()` on a 304, it will try to parse an empty body instead of using the copy it already has.

## Related Methods

- [HEAD](https://howhttpworks.com/methods/head): retrieval metadata without response content.
- [POST](https://howhttpworks.com/methods/post): process submitted content.

## Related Concepts

- [Cache-Control](https://howhttpworks.com/headers/cache-control)
- [If-None-Match](https://howhttpworks.com/headers/if-none-match)
- [Vary](https://howhttpworks.com/headers/vary)
