.NET and ASP.NET Core: the path of one request
How the .NET runtime and the ASP.NET Core host start, where settings and secrets come from, why middleware order decides safety, how Minimal APIs route and bind a request with code generated at build time, how a mediator hands it to one handler, and how errors, rate limits, CORS and CSRF are handled - step by step, with pictures.
In short
The {{brand}} server is one ASP.NET Core application on .NET 10. The built-in web server, Kestrel, reads each request and hands it to a pipeline of middleware: small steps that each check or prepare one thing, in a fixed order. At the end, routing picks one small function, a Minimal API endpoint, whose parameter-reading code was generated at build time. It sends one command to one handler, and any exception thrown by a handler becomes the right HTTP status in one central place.
After this lesson you can Explain how an ASP.NET Core app starts and where its settings come from, put middleware in a safe order and say why, build a Minimal API endpoint with a handler and clean error answers, and explain rate limiting, CORS and CSRF in an interview.
Words you need
- .NET runtime (CLR) The engine that runs C#. It loads compiled IL, turns it into machine code with the JIT compiler, and frees memory with a garbage collector.
- Kestrel The web server built into ASP.NET Core. It accepts connections, parses HTTP/1.1, HTTP/2 and HTTP/3, and hands each request to the app.
- Generic host The object that owns an app's dependency injection, configuration, logging and background services, and starts and stops them together.
- Configuration provider One source of settings: a JSON file, environment variables, the command line, a key vault. A provider added later overrides earlier ones.
- Options pattern Binding one settings section to a typed class - a Mail section to a MailOptions class - and injecting it as IOptions<MailOptions>.
- Dependency injection The framework creates services and hands each class what its constructor asks for. A scoped service lives for exactly one request.
- Middleware One step of the pipeline. It can answer at once (short-circuit) or call the next step and see the answer on the way back.
- Endpoint routing Matching the method and path to one endpoint early, so later middleware can read that endpoint's metadata: its rate limit, body size and policies.
- Minimal API Mapping a route straight to a function with MapGet or MapPost, without controller classes.
- Model binding Filling the function's parameters from the route, the query string, headers, the JSON body and services.
- Request delegate generator A source generator that writes the binding code at build time instead of building it while the app runs.
- Mediator A small object that takes a command or query and calls the one handler registered for it, so the endpoint never knows the handler.
The path of one request, end to end
Follow one request - a browser saving a note - from the network to the answer. Every ASP.NET Core app has these stages; only the middleware in the middle differs from app to app.
Routing reads the first line, binding reads the JSON body, and middleware reads the headers.
- 1. Kestrel reads the bytes Kestrel accepts the connection, parses the HTTP request and builds an HttpContext: one object holding the request, the response and the request's services.
- 2. Routing matches an endpoint Endpoint routing compares the method and path with every mapped route template and remembers the winner, so later steps can read its metadata.
- 3. Middleware runs in order Each middleware checks or prepares one thing: the real client address, security headers, CORS, sign-in, rate limits, the workspace. Any of them may answer early.
- 4. The request delegate binds parameters Code generated at build time reads the route id, the query or the JSON body into typed parameters, and asks DI for services.
- 5. The endpoint sends one command The endpoint adds what only the server knows - who is calling, in which verified workspace - and sends one command through the mediator.
- 6. The handler does the work The handler checks access, applies the rules, reads and writes the database, and returns a response record.
- 7. The answer goes back out The response record is written as JSON. On the way out, middleware adds headers and logs the status and the time taken.
- 8. Or a failure becomes a status If a handler throws, the exception handler near the top turns the exception into 400, 404, 409, 429 or a bare 500, with one JSON body shape for deliberate failures. Gates and limiters answer early, before any handler runs.
The .NET runtime and the host
- Compile The C# compiler writes IL, a portable instruction set, into a .dll file.
- Load The runtime loads the .dll when the app starts.
- JIT Methods are turned into machine code by the JIT the first time they run - quickly at first, then again with stronger optimizations for the methods called most (tiered compilation).
- Run The garbage collector frees memory nobody uses any more. Web apps use the server collector, built for many threads at once.
The JIT lets the same compiled file run on Windows, Linux and different CPUs, and optimize for the real workload.
async and await are what let one server handle thousands of requests. While a request waits for the database, its thread goes back to the thread pool and serves someone else; the request continues on any free thread when the answer arrives.
Pass the CancellationToken down, so work stops when the client goes away.
Every service in dependency injection has a lifetime: how long one instance lives and who shares it.
Never let a singleton hold a scoped service A singleton that keeps a scoped service (a captive dependency) keeps one request's database context alive for the whole life of the app and shares it between requests. ASP.NET Core in Development refuses this when scope validation is on.
- Build the configuration Settings files, then environment variables, then command-line arguments, then the key vault if one is set: the later provider wins.
- Register services Platform services first, then CORS and rate-limit policies, then each product module registers its own services.
- Build the app The DI container is built. From here on, the list of services is frozen.
- Refuse unsafe settings Readiness checks read the settings. Outside local development, a dangerous one stops the start with a message naming the setting.
- Assemble the pipeline Middleware is added in a fixed order, written once in one place with the reasons beside it.
- Map the endpoints Each product module maps its routes behind its own module switch.
- Run Kestrel starts listening, and the background services start beside it.
The middleware pipeline, and why order matters
Middleware is a chain of nested calls. Each step does some work, calls next, and runs again when the inner steps return - like the layers of an onion. A step that does not call next short-circuits: the request goes no deeper, and the answer travels back out through the steps it already passed.
Step 2, the exception handler, can only catch what happens inside it, which is why it sits near the outside.
- 1. Forwarded headers Restores the real client address and scheme from the trusted proxy. Rate limits and logs need the real address, so this comes first.
- 2. Exception handler Wraps everything below it. It can only catch what runs after it, so it sits near the top.
- 3. Security headers and HTTPS Adds nosniff, no framing, a strict content policy and HSTS to every answer the API produces, errors included; plain HTTP is redirected outside development.
- 4. CORS Answers the browser's preflight. A preflight carries no cookie, so it must be answered before sign-in could refuse it.
- 5. CSRF check A write signed by a session cookie must come from an allowed origin and carry something a cross-site form cannot send.
- 6. Authentication Reads the session cookie or a bearer token and finds out who is calling.
- 7. Authorization Applies endpoint policies, such as routes open to signed-out visitors.
- 8. Rate limiter Spends the request from a budget per person, per key or per address. It needs the caller, so it sits after sign-in.
- 9. Body and time limits Caps how large a body may be and how long a request may run, from the endpoint's own metadata.
- 10. Tenant and permission check In a multi-tenant app, verifies that the caller belongs to the tenant they name and that the feature is switched on: the last gate before the endpoint.
One place for the order Write the order once, in one place, with the reason beside every line. Nobody should have to search the code to learn what runs before an endpoint.
The options in the market
- ASP.NET Core Minimal APIs Routes mapped straight to functions; in {{brand}}, one small class per route.; Least ceremony: a route, a function and its parameters; Fast: no controller activation or filter pipeline by default; Works with the build-time request delegate generator and native AOT; No built-in structure: you choose and enforce your own; A large app without a folder rule ends with one endless Program.cs
- ASP.NET Core MVC controllers Classes of actions with attributes, filters and conventions.; Familiar to most .NET developers; Filters, model validation and conventions built in; Controllers grow into many unrelated actions; Built on runtime reflection: no request delegate generator, no native AOT
- FastEndpoints A library with one class per endpoint and its own conventions.; The request-endpoint-response pattern out of the box; Validation and versioning built in; A third-party dependency at the heart of every route; Its own conventions to learn and to upgrade
- Node.js (NestJS or Express) JavaScript or TypeScript on the server.; One language with the web app; A huge package ecosystem; One event loop: CPU-heavy work blocks other requests unless moved to workers; Types disappear at runtime; many small packages to keep safe
- Java Spring Boot A mature Java framework.; A deep enterprise ecosystem; Strong tooling; Heavier memory and start-up; More ceremony for small services
- Call a service directly The endpoint injects a service and calls a method on it.; The simplest possible; No extra layer to learn; Services grow into god classes with dozens of methods; Shared rules get copied between methods
- MediatR The best-known .NET mediator library.; Well known; Pipeline behaviors for logging or validation; A dependency at the center of every use case; Newer versions need a commercial licence for larger companies
- A small own mediator About a hundred lines: each command type finds its one handler.; No dependency and no licence; Fast: a typed dispatcher is cached per message type; Each use case is one small class, easy to test; No behaviors: cross-cutting work lives in middleware or the handler; Your own code to own and test
- Wolverine or MassTransit's mediator Messaging frameworks with an in-process mode.; The same model can later run over a message bus; A large framework for what is an in-process call
What {{brand}} chose, and why
{{brand}} has well over a thousand endpoints. With Minimal APIs, each route is one tiny class in the folder of its use case, beside its request, command and handler: a vertical slice. The build-time generator keeps start-up fast at that size. A small own mediator gives every use case exactly one handler, with no third-party library at the center of the product.
- No ready-made conventions: one route per class, no try/catch in endpoints and every route behind its module switch are enforced by architecture tests, not by a framework.
- No mediator behaviors: logging, limits and access run in middleware and endpoint metadata, which see every request - even one that never reaches a handler.
- Generator rules: request types must keep to a few shapes the generator can compile.
- Exceptions for expected failures: a missing record or a broken rule is thrown, not returned. An exception costs microseconds, small next to a database round trip, and handlers stay readable. A hot path that fails often should return a result instead.
- A typed, compiled platform: mistakes in a large codebase show up at build time, and one language covers the API, the background work and the tools.
- Fixed windows: one number a customer can read, cheap to count.
- Layered cookie protection: no single mechanism is trusted alone.
- Readiness checks: a bad setting stops the start with a clear message instead of failing on the first customer's request.
When another choice is right A team of Java experts, or a company standard on Spring, should not switch for this. A small service with five routes does not need a mediator - call the service. A public API that leans on many filters and validation conventions may be happier with controllers.
Configuration: providers, order and secrets
ASP.NET Core reads settings from a list of configuration providers and merges them into one tree of keys such as Mail:Host. When two providers set the same key, the one added later wins. That one rule explains nearly every surprise with settings.
- appsettings.json Defaults that are safe to commit: no secrets, empty connection strings, and a _comment note on nearly every section.
- appsettings.{Environment}.json Values for one environment - Development, Production - chosen by ASPNETCORE_ENVIRONMENT.
- A developer's local file An optional, untracked file for one developer's own machine; tests never load it.
- Environment variables What containers and orchestrators set. Mail__Password means Mail:Password: two underscores stand for the colon.
- Command-line arguments --Mail:Host=mail.example.com for one-off runs and tools.
- A secret store (for example Azure Key Vault) When a vault address is configured, secrets load from the vault after the other sources. The machine reaches it with its cloud identity or, on a developer's laptop, the developer's own sign-in, so there is no password for reaching the passwords.
The options pattern turns a section into a typed class. Instead of reading strings everywhere, a class asks for IOptions<MailOptions> and gets numbers as numbers and defaults in one place.
Check settings at start, not on the first customer's request The built-in way is options validation with ValidateOnStart. {{brand}} uses its own readiness checks: each reads one setting and answers ready, warning or blocker. Outside local development a blocker - a missing signing key, for example - stops the process at once, with a message naming the setting to fix.
A vault provider can reload on a timer, but a value already copied into an IOptions<T> object stays as it was, so a rotated secret usually needs a restart.
Endpoint routing and Minimal APIs
Endpoint routing works in two halves. Early in the pipeline, the matcher picks the endpoint; at the very end, the endpoint runs. Between the two, every middleware can read the chosen endpoint's metadata - its rate-limit policy, its body limit, whether it allows signed-out callers. A WebApplication adds both halves for you, around your own middleware.
- One route, one class Each route is one small class in its use-case folder, beside the request, the command and the handler. Routes are registered when the app starts, grouped by module, so adding a route never edits another file.
- Mapped inside the module's group All of a module's routes are mapped inside one route group that carries the module's switch, so no route can forget it. An exception is declared explicitly.
- Versioned prefix The API routes sit under a version prefix, /api/v1; a breaking change would ship under a new one.
- Guarded by tests An architecture test can fail the build if a rule is broken, such as an endpoint class that maps two routes or catches exceptions.
Model binding and the request delegate generator
If binding fails, for example a bad GUID or invalid JSON in the body, the framework answers 400 before your function runs.
Something must turn those rules into code that runs on every request: read the route value, parse the GUID, read and deserialize the body, resolve the service, call your function and write the result. That code is the request delegate. By default ASP.NET Core builds it at runtime from expression trees, when the endpoint is first used. The request delegate generator writes the same code at build time as plain C#, and the compiler swaps your MapGet call for it with interceptors.
- Runtime factory (the default) Builds each delegate from expression trees on first use.; Supports every handler shape; Nothing to switch on; Every new process pays the cost again, on its first requests; Reflection and dynamic code: not friendly to trimming or native AOT; Many hosts starting at once compete for the JIT compiler
- Request delegate generator Writes the delegates as C# at build time; one switch in the project file.; No runtime compilation: a fast cold start; Binding mistakes surface at build time; A required step towards native AOT; A few handler shapes it cannot compile; Generated code to read when debugging
- Find the map calls The generator looks at every MapGet, MapPost, ... whose handler it can see at the call site.
- Read the handler's signature For each parameter it decides the binding source - route, query, body, header, service - from types and attributes.
- Write the delegate It emits C# that parses, validates and calls the handler, then writes the result: no reflection left for runtime.
- Intercept the call An interceptor attribute tells the compiler to call the generated method instead of the original MapGet at that exact line.
- Compile everything together The generated file is compiled with your code; you can read it under the obj folder with EmitCompilerGeneratedFiles.
What the generator changes in practice Cold start. A big API used to build hundreds of delegates during its first requests, and many test hosts starting at once queued for the compiler. Moving that work to build time cut a long test run from tens of minutes to under a minute, and a new deployment's first visitor no longer waits either.
The rules it brings Write the handler where it is mapped - a method group or a lambda - so the generator can see it. A record bound with [AsParameters] that has an optional text or array field must use init properties, not a positional constructor: the generator writes typeof(string?), which C# refuses (error CS8639). Bound properties have no default values, so optional numbers are nullable and the handler applies the default.
Native AOT compiles a whole app to machine code ahead of time: a tiny start-up time and memory use, but no runtime code generation, and only the code a trimmer can prove is used. The generator removes one big obstacle. Native AOT also needs the rest of the app to avoid reflection, and some libraries, such as EF Core, still treat it as experimental. The faster cold start is the part that pays off for most apps today.
The mediator: one command, one handler
In CQRS style, a use case that changes data is a command and one that only reads is a query. Each is a small record that names its result type. A mediator finds the one handler registered for that record's type and calls it. The endpoint depends on the mediator alone, so it never grows a list of services.
- Scan once at start Each module's assembly is scanned; every handler class is registered in DI as scoped, because it uses the request's database context.
- Send The endpoint calls send with a command; the mediator reads the command's runtime type.
- Find the dispatcher A small typed dispatcher for that type is built on first use and cached, so later sends do no reflection at all.
- Resolve and call The dispatcher asks DI for the handler of exactly that command type and awaits it.
- Return The handler's response record travels back to the endpoint, which wraps it in an HTTP result such as 200 OK.
Cross-cutting work without behaviors Libraries like MediatR wrap handlers in behaviors for logging or validation. {{brand}}'s mediator has none on purpose: logging, limits and access run in middleware and endpoint metadata, which see every request, whether or not it reaches a handler.
Errors: from exception to status code and problem body
A handler that finds a problem throws a typed exception: not found, conflict, invalid input. Nothing catches it on the way up. Near the top of the pipeline, ASP.NET Core's exception handler middleware calls the registered IExceptionHandler, which maps the type to a status and writes the body. Endpoints therefore contain no try/catch at all. Gates and limiters - sign-in, the cross-site check, the rate limiter, body and time limits - write their own answers early, before any handler runs.
- Catch The middleware catches the exception and calls the registered handler with the HttpContext.
- Is the client still there? If the client already disconnected, write nothing; log the exception only if it was a real failure.
- A deliberate failure A known type maps to its status. The sentence is rendered in the caller's language; the code stays the same for programs.
- An unexpected failure Log the full exception with the correlation id and the route template, then answer a bare 500 in problem details form.
- Hand the id back The same id goes into a response header, so a support ticket can quote it and find the log line at once.
Why 404 for hidden things If a stranger asks for record 42 and gets 403, they have learned that record 42 exists. Answering 404 for both missing and hidden records gives an attacker nothing to count or guess with.
Rate limiting: a budget per caller
A rate limiter gives each caller a budget of requests per time window and answers 429 Too Many Requests with a Retry-After header when it runs out. ASP.NET Core has one built in: a middleware plus named policies, each choosing an algorithm and a partition key - whose budget this request spends.
- Fixed window N requests per window; the count resets when the window ends.; Simple and cheap; Easy to state as one number; A burst at the edge of two windows can reach twice N
- Sliding window The window is cut into segments that slide forward.; Smooths the edge burst; More counters to keep per partition
- Token bucket Tokens refill at a steady rate; each request takes one.; Allows a short burst, then a steady pace; Harder to explain to customers as one limit
- Concurrency At most N requests in flight at once.; Protects heavy work such as exports; Chains with a time window; Says nothing about requests per minute on its own
- Global limiter Every request first spends from a global budget for its partition.
- Endpoint policy Routes that cost more, such as exports, add a named policy with a tighter budget.
- Chained limits Heavy work chains limiters: a concurrency limit first, so a refused request spends nothing of the window's budget, then the window.
- Refusal Out of budget: 429, a Retry-After header in seconds and a stable error code.
The counters live in memory A framework's built-in limiter usually counts in the process's own memory. For one strict budget shared by many servers use a shared store such as Redis, or limit at the edge.
CORS and CSRF explained
- Origin Scheme, host and port, such as https://app.example.com. Two URLs are same-origin only when all three match.
- Same-origin policy The browser rule that a page's script may not read answers from another origin unless that origin allows it.
- CORS Cross-Origin Resource Sharing: response headers in which a server lists which other origins may read its answers.
- Preflight An OPTIONS request the browser sends first for calls with a JSON body, custom headers, PUT or DELETE, asking permission.
- Credentials Cookies sent on a cross-origin call. Allowed only when the server names the exact origin - never the wildcard *.
- CSRF Cross-site request forgery: another site makes your browser send a request that carries your cookie.
- SameSite A cookie attribute. Strict never sends the cookie on requests started from another site; Lax still sends it when you follow a link.
- The page wants to call A script on https://app.example.com calls PUT https://api.example.com/notes/7 with a JSON body.
- The browser asks first It sends OPTIONS with Origin, Access-Control-Request-Method: PUT and the headers it wants to send.
- The server answers The CORS middleware compares the Origin with its allow list and answers the Access-Control-Allow-Origin, Methods, Headers, Credentials and Max-Age headers.
- The browser decides If the answer allows it, the real PUT goes out; otherwise the browser blocks it and the script sees only a network error.
- The answer is cached Max-Age lets the browser reuse the answer, so later calls skip the preflight.
CORS protects people, not servers CORS is enforced by browsers only. curl, scripts and other servers ignore it entirely. It decides which web pages may read your API's answers with a visitor's cookies; it never replaces authentication, and on its own it does not stop CSRF.
- You are signed in Your browser holds a session cookie for app.example.com.
- You open another site evil.example hides a form that posts to app.example.com/api/transfer.
- The browser attaches the cookie Without protection, the browser sends your cookie with that cross-site request. A plain form post needs no preflight.
- The server obeys The server sees a valid session and does the action. The attacker never reads the answer - and never needed to.
- SameSite cookies A Strict or Lax cookie is not sent on cross-site requests.; One attribute, no code; Supported by every current browser; Same-site is not same-origin: a sibling subdomain counts as the same site; Lax still sends the cookie on a link that is followed
- Anti-forgery tokens A secret token in each form, checked by the server.; Classic and strong for server-rendered forms; Token plumbing in every form and every single-page-app call
- Custom header and origin check Writes must carry a header a cross-site form cannot set, from an allowed Origin.; Simple for a single-page app that uses fetch; A form or an image tag cannot add custom headers, and a script on another origin needs a preflight the server refuses; Every client must remember to send the header; Long-lived connections such as WebSockets often cannot carry custom headers, so they rely on the origin check
- Bearer tokens only No cookie at all: the token travels in a header.; CSRF disappears: browsers never add headers on their own; The token must sit where scripts can read it, so one XSS bug can steal it
{{brand}} follows the layered approach described above: a hardened session cookie plus a second check on writes. Pick the layers that suit your own app. State-changing work must never hang on a GET, because browsers and crawlers follow links with cookies.
Try it yourself: a tiny API that shows the pipeline
In half an hour you can see every idea of this lesson working, with the free .NET SDK and curl. Watch how the order of two lines changes what you see.
- Create the app Install the .NET SDK and run dotnet new web -n PathDemo.
- Paste the program Replace Program.cs with the code below: a timing middleware, an exception handler, a rate limit and one endpoint.
- Hit the constraint Open a second terminal and call /books/not-a-guid. You should see HTTP/1.1 404 and no JSON body: the route constraint did not match before your code ran.
- Hit the handler Call /books/3f2a6c1e-0000-0000-0000-000000000001. The handler throws, the exception handler answers 404 with a JSON body, and the first terminal prints a timing line.
- Hit the limit Keep calling it: within one minute, the sixth call answers 429 with a Retry-After header.
- Swap the order Move the timing middleware below app.UseExceptionHandler() and call the GUID route again. The 404 still comes back, but the timing line is gone: the exception passed straight through the timing step and never let it log. Put it back above and the line returns.
- Switch on the generator Add the two properties to PathDemo.csproj, build, and read the generated request delegate under obj.
Common mistakes
- Middleware in the wrong order: an exception handler below the code that fails, a rate limiter before sign-in, CORS after authentication. Write the order once, with a reason on every line.
- Trusting forwarded headers from anyone: X-Forwarded-For is only text a client can send. Honour it only from your own proxy, or every limit keyed on the address can be dodged.
- Secrets in a settings file or in logs: committed files leak. Keep secret values empty in appsettings.json and supply them from the environment or a vault. Tokens in query strings, such as share links or feeds, end up in log files too, so log the route template instead of the raw URL.
- Any origin with cookies: ASP.NET Core refuses AllowAnyOrigin together with credentials, and code that echoes any Origin back is worse - every site may then call your API as the visitor. List exact origins.
- Blocking on async work: calling .Result or .Wait() holds a pool thread. Under load every thread waits and the server stops answering. Use await all the way and pass the CancellationToken.
- A singleton that holds a scoped service: it keeps one request's database context alive for the whole life of the app. Keep long-lived services free of per-request ones, or create a scope when you need one.
Interview questions
- Why does middleware order matter? Each middleware only wraps what comes after it. The exception handler goes near the top to catch everything, CORS before authentication because preflights carry no cookie, and the rate limiter after authentication so it knows whose budget to spend.
- In what order are configuration providers applied, and IOptions, IOptionsSnapshot or IOptionsMonitor? In the order they are added, and a later provider overrides an earlier one, so deployment settings can override file defaults without editing files. The default is appsettings.json, the environment's file, user secrets in development, environment variables and the command line; a key vault added after them wins over those. IOptions is computed once, IOptionsSnapshot is re-read per request, and IOptionsMonitor sees reloads at once and raises change events.
- Why does async/await let a server handle more requests? While a request waits for I/O, its thread returns to the pool and serves another request; the work resumes on any free thread. Blocking with .Result keeps the thread, so the pool starves and requests queue.
- Minimal APIs or controllers? Minimal APIs map a route straight to a function with less ceremony, and they work with the build-time generator and native AOT. Controllers bring filters and conventions that suit teams used to MVC. With Minimal APIs you must choose a structure yourself, such as one class per route in vertical slices.
- What does the request delegate generator do? It writes each Minimal API endpoint's parameter-binding code at build time and swaps it in with interceptors. That removes runtime expression compilation, speeds up cold starts and is required for native AOT.
- Why use a mediator, and what does it cost? Endpoints depend on one dispatcher instead of many services, and each use case becomes a small, testable handler. The cost is indirection: go-to-definition no longer jumps to the handler, so naming conventions must make the pair obvious.
- How do you turn exceptions into HTTP answers? Register an IExceptionHandler and call UseExceptionHandler near the top of the pipeline. Map typed exceptions to statuses with one body shape, and answer unexpected ones with a bare 500 and a correlation id - never a stack trace.
- Explain singleton, scoped and transient services. What is a captive dependency? A singleton lives for the whole app, a scoped service for one request, and a transient one is new each time. A captive dependency is a singleton that holds a scoped service, such as a database context: the request-bound object then lives for ever and is shared between requests, causing wrong data and thread-safety bugs.
- What is the difference between CORS and CSRF? CORS is the browser rule that decides which other origins may read your API's answers. CSRF is an attack in which another site makes the browser send a request with your cookie. CORS does not stop CSRF, because a plain form post needs no preflight.
- How would you rate-limit an API behind a proxy? Trust forwarded headers only from the known proxy, so the real client address is used. Partition by user id when signed in, by token for scripts and by address otherwise, and answer 429 with Retry-After.
Related
- /help/technology/architecture
- /help/technology/sign-in
- /help/technology/access