Matcher #

By default, every directive you write inside a Caddyfile site block applies to all HTTP requests arriving at that site. For example, if you write reverse_proxy localhost:3000, then every request — whether to the homepage /, an image asset /logo.png, or the API endpoint /api/v1/users — gets forwarded to that backend server.

However, in real-world scenarios, you rarely want such uniform behavior. You often need conditional logic like: “enable compression for all files except images”, “require password authentication only for the /admin/* routes”, or “forward requests to the backend server only if they go to the /api/ path”. This is where the Request Matcher plays a vital role. Matchers are Caddy’s built-in mechanism for filtering and routing traffic granularly based on various request properties.


Request Matcher Evaluation Logic #

Caddy evaluates incoming requests through a series of matcher filters before applying the relevant directive.

To visualize this request matching flow, look at the evaluation flowchart below:

flowchart TD
    Request["Incoming Request (GET /api/users)"] --> EvalHost{"1. Evaluate Host Matcher?"}
    EvalHost -- "No Match" --> Ignore["Ignore Block / Skip"]
    EvalHost -- "Match" --> EvalPath{"2. Evaluate Path Matcher?"}
    EvalPath -- "No Match" --> Ignore
    EvalPath -- "Match" --> EvalMethod{"3. Evaluate Method Matcher?"}
    EvalMethod -- "No Match" --> Ignore
    EvalMethod -- "Match" --> CheckCEL{"4. Evaluate CEL Expression (If any)?"}
    CheckCEL -- "Evaluates False" --> Ignore
    CheckCEL -- "Evaluates True" --> ApplyDirective["Apply Directive to Request"]

Two Ways to Write Matchers #

The Caddyfile supports two matcher writing styles depending on the complexity of the condition you need:

1. Inline Matcher (Directly on the Directive) #

An inline matcher is written directly on the same line as the directive. This is the fastest and cleanest way for simple matching conditions that only involve URL paths.

example.com {
    # The inline '/api/*' matcher limits reverse_proxy to that path only
    reverse_proxy /api/* localhost:8080
    
    # Serve static files for all remaining requests
    file_server
}

[!NOTE] By default, every inline matcher in Caddy is treated as a Path Matcher if it starts with a slash (/) or asterisk (*) character.

2. Named Matcher #

A named matcher is defined separately using the @ character followed by a custom name of your choosing. Named matchers are very useful for defining complex conditions involving multiple parameters (like combined path, method, IP address, and header), or when the condition needs to be reused across several different directives.

example.com {
    # Named matcher definition '@adminAccess'
    @adminAccess {
        path /admin/*
        method GET POST
        not remote_ip 192.168.1.0/24
    }
    
    # Use that matcher in a basicauth directive
    basicauth @adminAccess {
        admin $2a$14$hashexample...
    }
}

Named Matcher vs Inline Matcher: When to Choose Which? #

Choosing the right matcher writing style greatly determines the maintainability of your Caddyfile as domains and application logic grow.

When to Use Inline Matchers? #

  • Simple Single Routes: When you only need matching based on URL path (e.g., /images/* or /health).
  • Concise Syntax: Avoid creating new blocks for very simple routes, keeping the Caddyfile short.

When to Use Named Matchers? #

  • Multi-Criteria Combinations: When you need to match requests by IP address and HTTP method simultaneously (e.g., “Only accept POST methods from the Office IP”).
  • DRY Principle (Don’t Repeat Yourself): When the same condition must be used by several different directives at once. For example, you want to detect API requests to enable compression, add CORS headers, and route the proxy:
example.com {
    # Define the matcher once
    @apiRequest {
        path /api/*
        header Accept application/json
    }

    # Reuse the '@apiRequest' matcher across 3 different directives
    header @apiRequest Access-Control-Allow-Origin "https://app.example.com"
    encode @apiRequest gzip
    reverse_proxy @apiRequest localhost:8080
}

List of Available Standard Matchers #

Caddy provides many built-in matcher types ready to filter requests based on various aspects of the HTTP protocol:

1. path & path_regexp (Matching URL Paths) #

Matches requests based on the URL path requested by the client browser.

# Match the exact path '/login' only
@exact path /login

# Match the '/static/' prefix (using the '*' wildcard)
@static path /static/*

# Match several paths at once
@assets path /css/* /js/* /images/*

# Match specific file extensions
@phpFiles path *.php

If standard wildcard matching isn’t flexible enough, you can use path_regexp with Regular Expressions:

# Match the /users/[number]/profile route
@userProfile path_regexp user ^/users/([0-9]+)/profile$

# You can use the capture group variable in other directives
rewrite @userProfile /profile-handler?user_id={re.user.1}

2. host (Matching Hostname/Domain) #

Very useful inside site blocks serving multiple domains at once (multi-domain site block).

example.com, api.example.com, admin.example.com {
    @isApi host api.example.com
    @isAdmin host admin.example.com
    
    # Route traffic based on the host matcher
    reverse_proxy @isApi localhost:8080
    reverse_proxy @isAdmin localhost:9000
    
    # Default for example.com
    file_server
}

3. method (Matching HTTP Methods) #

Filters traffic based on the HTTP action sent by the browser (like GET, POST, PUT, DELETE).

# Match only if the method is POST
@postRequest method POST

# Match read-only data operations
@readOps method GET HEAD

4. header (Matching HTTP Headers & Cookies) #

Filters requests based on the presence or value of specific HTTP headers, including Session and Cookie.

# Match if the Authorization header contains any value (wildcard '*')
@hasToken header Authorization *

# Match if the request asks for JSON format
@expectsJSON header Accept application/json

# Match if the browser used is Google Chrome
@isChrome header User-Agent *Chrome*

# Match a specific cookie (Caddy reads Cookie as an HTTP header)
@activeSession header Cookie *session_id=*

5. remote_ip (Matching Client IP Addresses) #

Restricts access based on the requesting client’s IP address. Supports single IPs and CIDR notation.

# Match if the IP comes from the local network (LAN)
@internalNetwork remote_ip 10.0.0.0/8 192.168.1.0/24

# If Caddy is behind Cloudflare/Load Balancer, use the 'forwarded' option
# so Caddy reads the client's real IP from the X-Forwarded-For header
@trustedClient remote_ip forwarded 203.0.113.50

6. query (Matching Query Parameters) #

Matches requests based on the query string at the end of the URL.

# Match if the URL contains '?debug=true'
@debugMode query debug=true

# Match if the 'page' parameter exists with any value
@paginated query page=*

7. file (Matching Physical File Existence) #

Checks whether the file requested by the client actually exists on the server disk before deciding the next action.

example.com {
    root * /var/www/html
    
    # Match if the requested file does NOT exist on disk
    @fileNotFound {
        not file {path}
    }
    
    # Rewrite the request to index.html if the file isn't found (SPA Routing)
    rewrite @fileNotFound /index.html
    
    file_server
}

8. protocol (Matching HTTP Protocol) #

Filters requests based on the protocol version used by the client.

# Identify WebSocket requests
@isWebSocket {
    protocol http
    header Upgrade websocket
}

# Identify HTTP/3 (QUIC) requests
@isHTTP3 {
    protocol h3
}

Standard Matcher Capability Summary Matrix #

Here’s a comparison summary table of the capabilities of each core request matcher in Caddy:

Matcher NameMain EvaluationExample SyntaxMain Production Use
pathURL Path Stringpath /api/*High-level URL routing.
path_regexpURL Path Regexpath_regexp ^/user/\d+$Extracting ID parameters from URLs.
hostHTTP Host Domainhost api.domain.comMulti-domain routing in one block.
methodHTTP Methodmethod GET POSTRestricting application read/write routes.
headerHTTP Header Key/Valueheader Cookie *session*Security header and session cookie checks.
remote_ipIP Address & CIDRremote_ip 10.0.0.0/8IP whitelisting and intranet restrictions.
queryURL Query Parametersquery debug=trueTroubleshooting parameter detection.
fileDisk Filesystemfile {path}Router fallback for Single Page Apps (SPA).
protocolHTTP Version / TLSprotocol httpsSpecial handling for WebSocket / HTTP3 traffic.

Logical Operators: NOT, AND, OR #

By default, if you write several conditions inside one named matcher block, Caddy evaluates them with AND logic (all conditions must be met for the matcher to produce true).

However, you can build more complex logic using the following operators:

1. The not Operator (Negation/Inverse) #

Flips the evaluation result of the condition inside it.

# Match if the path does NOT start with /public/
@notPublic {
    not path /public/*
}

# Match if the sender IP is NOT from the LAN
@outsideOffice {
    not remote_ip 10.0.0.0/8
}

2. OR Logic (Any Condition Met) #

To test OR conditions, you can define several separate named matchers and point them at the same directive, or use the Expression Matcher based on the CEL language.

# Option A: Using several separate named matchers
@adminRoute path /admin/*
@superRoute path /superuser/*

# Both routes are protected by the same basicauth
basicauth @adminRoute { ... }
basicauth @superRoute { ... }

CEL Expressions (Common Expression Language) #

For very complex advanced conditional logic (like string operations, nested OR logic, or dynamic runtime variable checks), Caddy provides the expression matcher using the CEL (Common Expression Language) parser.

example.com {
    # Complex condition: Match only if the method is POST, content type is JSON,
    # and it comes from an IP outside the office network
    @specialCondition {
        expression {
            request.method == "POST" &&
            request.header["Content-Type"][0] == "application/json" &&
            !req_ip("10.0.0.0/8")
        }
    }
    
    reverse_proxy @specialCondition localhost:8080
}

Some CEL functions that are often useful in production:

  • request.uri.path.startsWith('/api/') — Checks the path prefix.
  • request.header['User-Agent'][0].contains('bot') — Bot detection.
  • req_ip('192.168.1.0/24') — Dynamic IP matching.

Matcher Application Patterns in Production #

Here are some matcher implementation templates that are very useful for securing and tidying your production server configuration:

1. API vs Frontend Traffic Split (Single Domain) #

Often you want to serve a static frontend and a backend API using the same domain to avoid CORS issues.

example.com {
    # 1. Define Matchers for the API and Static Assets
    @apiPath path /api/*
    @staticAssets path /static/* /assets/*
    
    # 2. The API is forwarded to the backend application
    reverse_proxy @apiPath localhost:8000
    
    # 3. Static assets are served from a dedicated folder
    file_server @staticAssets {
        root /var/www/app/static
    }
    
    # 4. All other requests are routed to the React/Vue SPA
    @spaFallback {
        not path /api/*
        not file {path}
    }
    rewrite @spaFallback /index.html
    file_server {
        root /var/www/app/dist
    }
}

2. Dynamically Blocking Malicious Bots (Scrapers/Crawlers) #

You can block access to your site for data-scraping bots that burden the server using a User-Agent header combination.

example.com {
    # Expression matcher to detect malicious bot User-Agents
    @evilBot {
        expression {
            request.header["User-Agent"][0].contains("scrapy") ||
            request.header["User-Agent"][0].contains("wget") ||
            request.header["User-Agent"][0].contains("curl")
        }
    }
    
    # Send an instant 403 Forbidden status without processing the request to disk/backend
    respond @evilBot "Access Denied" 403
    
    reverse_proxy localhost:3000
}

3. Layered Security for an Admin Dashboard #

Restricts admin page access to only the office internal network (IP Whitelisting) while still requiring password verification.

admin.company.com {
    # Matcher 1: Detect requests coming from OUTSIDE the office network
    @outsideOffice {
        not remote_ip 10.0.0.0/8 192.168.1.0/24
    }
    
    # Block outside access with status 403
    respond @outsideOffice "Unauthorized Network Access" 403
    
    # Matcher 2: All requests must pass basicauth
    basicauth {
        spv $2a$14$hashspv...
    }
    
    reverse_proxy localhost:9000
}

Debugging Request Matchers #

If your matcher configuration isn’t working as expected (for example, routes don’t match or requests are blocked when they should be allowed), you can debug using these two methods:

1. Use the caddy adapt Command #

Adapt your Caddyfile to JSON to see how AND/OR logic and negation are turned into internal matcher objects. Check for any wrong nested structures.

caddy adapt --config Caddyfile | jq .apps.http.servers

The JSON adaptation output shows how named matchers are translated into a structured match array, making it easier to spot logical syntax errors.

2. Temporarily Add a respond Directive #

To confirm whether your request is actually caught by a particular matcher, put a temporary respond directive under that matcher:

example.com {
    @testMatcher {
        path /test-path/*
        method POST
    }
    
    # Return test text instantly for verification
    respond @testMatcher "Matcher Condition Met!" 200
    
    reverse_proxy localhost:3000
}

If you access /test-path/ with POST and receive that text, your matcher is working correctly. After testing, you can remove the respond line.


Summary #

  • Request Matchers are used to apply directives conditionally only to specific request criteria.
  • Inline Matchers (like /api/*) are perfect for simple path route filtering on the same line as the directive.
  • Named Matchers start with the @ symbol (e.g., @access) to build complex, structured filter combinations.
  • Conditions inside one named matcher block are evaluated with AND logic by default.
  • Use the not operator to invert a matching condition (like blocking IPs or excluding files).
  • The expression matcher based on the CEL language provides full programming flexibility for advanced routing logic in Caddy.

← Previous: Directive   Next: Snippet & Import →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact