How HTTP Works

Method · safe · idempotent

> GET /users/42 HTTP/1.1>  Host: api.example.com>  Accept: application/json

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.

Reviewed 6 min readbeginner5 sourcesTry itMarkdown
Safe
Yes
Idempotent
Yes
Cacheable
Yes
Request body
No defined semantics
Response body
Yes
Spec
RFC 9110 §9.3.1
On this page

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:

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

RFC 9110 §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.

Query Parameters

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

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:

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 covers both behaviors.

Real-World Examples

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

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/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:

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, including the need to handle errors after a transfer has partially started.

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

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 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:

HeaderExample valueDebugging use
Acceptapplication/jsonCompare what the client requested with the returned Content-Type.
Accept-Languageen-US,en;q=0.9Check whether language negotiation changes the representation.
Accept-Encodinggzip, brCompare compressed and uncompressed variants.
If-None-Match"catalog-v7"Validate the representation already stored by the client.
Cache-Controlno-cacheRequest 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; a successful byte-range request can return 206. A conditional request can return 304. A 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 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

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:

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, 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.

  • HEAD: retrieval metadata without response content.
  • POST: process submitted content.

Check your understanding

Guided Lab

Practice GET with query parameters

Use the playground to send a GET request, then change only the query string to see how reads are expressed without a request body.

Practice GET
  1. Send the default GET preset and inspect the URL, headers, and response.
  2. Edit the query string to request a different product id and send it again.
  3. Confirm that the request still has no body and that the URL fully describes the lookup.

Look for: The key data for a simple GET should stay in the path and query string, not in a request payload.

Why it matters: GET becomes intuitive when you treat the URL itself as the selector for the representation you want.

Interactive Check

Choose the safer method

You need a retry-friendly endpoint that only fetches order details and should never charge or mutate anything. Which method best matches that contract?

Choose the safer method

In practice

Express.js
// GET /users — list with pagination
app.get('/users', async (req, res) => {
  const { page = 1, limit = 20 } = req.query
  const users = await db.users.findAll({ page: +page, limit: +limit })
  res.json({ users, page: +page, limit: +limit })
})

// GET /users/:id — single resource
app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id)
  if (!user) return res.status(404).json({ error: 'Not found' })
  res.json(user)
})
Next.js App Router
// app/api/users/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = Number(searchParams.get('page') ?? 1)
  const users = await db.users.findAll({ page })
  return Response.json({ users, page })
}

// app/api/users/[id]/route.ts
export async function GET(
  _req: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.users.findById(params.id)
  if (!user) return new Response(null, { status: 404 })
  return Response.json(user)
}
Django
# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def user_list(request):
    page = int(request.GET.get('page', 1))
    qs = User.objects.values()[(page - 1) * 20 : page * 20]
    return JsonResponse({'users': list(qs), 'page': page})

@require_GET
def user_detail(request, user_id):
    try:
        user = User.objects.get(pk=user_id)
        return JsonResponse(user.to_dict())
    except User.DoesNotExist:
        return JsonResponse({'error': 'Not found'}, status=404)

Frequently asked questions

What is the HTTP GET method?

GET fetches the resource at a URL without changing it. It is the method behind every page load and most API reads. It is safe, meaning the client is not asking for a state change; side effects such as logging are fine.

What is the difference between GET and POST?

GET retrieves a representation; POST submits content for resource-specific processing. GET responses are cacheable under HTTP cache rules. POST responses can also be cached with explicit freshness and a Content-Location matching the target URI.

Can GET requests have a body?

Not usefully. HTTP gives a GET body no defined meaning, and the browser Fetch API refuses to send one. Put lookup parameters in the query string, or use an endpoint whose method defines what a request body means.

Are GET requests cached?

They can be. Whether a cache stores and reuses one depends on the status code, the response headers, and the cache itself. Cache-Control: no-store keeps it out of caches entirely; no-cache lets caches store it but makes them revalidate before each reuse.

What is the maximum URL length for GET requests?

HTTP itself sets no limit. Browsers, proxies, and servers each have their own, so when a long URL fails, find the hop that rejected it (often with a 414) rather than assuming a 2,000-character ceiling.

Is GET safe and idempotent?

Yes, both. Safe means it only asks to read, and idempotent means repeating it has the same intended effect on server state as sending it once. The response itself can still differ between requests if the data changed.

Sources

  1. MDN Web Docs: GETdeveloper.mozilla.org
  2. RFC 9110: GET Methodrfc-editor.org
  3. HTTP Semanticshttpwg.org
  4. Fetch Standard: Request constructorfetch.spec.whatwg.org
  5. RFC 9111: HTTP Cachingrfc-editor.org

Keep going

Browse /search