Describing response headers and cookies

Describing headers

You also must specify which headers are returned (if any).

When using “real endpoints”, you can provide headers parameter to ResponseSpec if there are headers you want to describe. HeaderSpec is here to help. You can create both required=True (always must be present on the response object) and required=False headers (might be missing in some cases):

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Tue, 14 Apr 2026 17:09:27 GMT
server: uvicorn
X-Created: f3738563-82c2-4294-b5bc-09a59456836d
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@ourdomain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Tue, 14 Apr 2026 17:09:27 GMT
server: uvicorn
X-Created: dd7daa01-0a5b-4b1d-b6a0-c79a8e636cd3
X-Our-Domain: true
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 30
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@ourdomain.com"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK",
            "headers": {
              "X-Created": {
                "required": true,
                "schema": {
                  "type": "string"
                }
              },
              "X-Our-Domain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

Note

All headers from the response objects are checked. We will report:

  • Required headers that exist in the spec, but not on the response

  • Any headers that exist on the response, but not present in the spec

Content-Type header is the only one that is always added automatically.

With “raw endpoints” you can also use NewHeader marker which can set headers with known values to the final response.

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Tue, 14 Apr 2026 17:09:28 GMT
server: uvicorn
Content-Type: application/json
X-Created: true
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK",
            "headers": {
              "X-Created": {
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

If you need headers with not static, but dynamic values, use “real endpoints” and pass headers dict to to_response() method.

The last important thing about headers is skip_validation attribute. It is used to describe headers that:

  1. Will be set in the response by someone else outside the framework, like HTTP proxy or Django’s own middleware. See django.contrib.sessions.middleware.SessionMiddleware as a notable example

  2. Will be validated to be NOT present in the response from our framework. Since it is designed to be added later, it should not be already present

Important

Header definitions are case insensitive according to the HTTP spec. Session and session is the same header.

Describing cookies

Warning

Some may say that returning cookies is not “RESTful”, because cookies is an implicit state, that RESTful APIs must not have. Be careful, only use this feature when you need to.

See: https://parottasalna.hashnode.dev/is-it-okay-to-add-cookie-to-a-rest-api

We also support setting and validating response cookies.

You can use NewCookie to add new cookies with statically known values to “raw endpoints”. Or CookieSpec with both types of endpoints to describe response cookies.

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 201 Created
date: Tue, 14 Apr 2026 17:09:28 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Set-Cookie: user_created=true; expires=Tue, 14 Apr 2026 17:26:08 GMT; Max-Age=1000; Path=/; SameSite=lax

{"email":"user@wms.org"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created",
            "headers": {
              "Set-Cookie: user_created": {
                "required": true,
                "schema": {
                  "example": "user_created=123",
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

And you can set any cookies to django.http.HttpResponse.cookies with “real endpoints”. Since we have strict schemas, it is required to describe the set cookies with CookieSpec:

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@ourdomain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 201 Created
date: Tue, 14 Apr 2026 17:09:29 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 30
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Set-Cookie: user_id=f124ac16-c122-4e5c-bec0-0960adc09fc0; Path=/; SameSite=lax
Set-Cookie: session=true; expires=Tue, 14 Apr 2026 17:26:09 GMT; Max-Age=1000; Path=/; SameSite=lax

{"email":"user@ourdomain.com"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created",
            "headers": {
              "Set-Cookie: session": {
                "schema": {
                  "example": "session=123",
                  "type": "string"
                }
              },
              "Set-Cookie: user_id": {
                "required": true,
                "schema": {
                  "example": "user_id=123",
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

The last important thing about cookies is skip_validation attribute. It is used to describe cookies that:

  1. Will be set in the response by someone else outside the framework, like HTTP proxy or Django’s own middleware. See django.contrib.sessions.middleware.SessionMiddleware as a notable example

  2. Will be validated to be NOT present in the response from our framework. Since it is designed to be added later, it should not be already present

Note

All cookie parts are validated by default. Except expires field, because it is relative to the current time.

Important

Cookie definitions are case sensitive according to the HTTP spec. Session and session are two different cookies.