1from __future__ import annotations
2
3import typing as t
4from datetime import datetime
5from datetime import timedelta
6from datetime import timezone
7from http import HTTPStatus
8
9from ..datastructures import CallbackDict
10from ..datastructures import ContentRange
11from ..datastructures import ContentSecurityPolicy
12from ..datastructures import Headers
13from ..datastructures import HeaderSet
14from ..datastructures import ResponseCacheControl
15from ..datastructures import WWWAuthenticate
16from ..datastructures.cache_control import _CacheControl
17from ..http import COEP
18from ..http import COOP
19from ..http import CORP
20from ..http import dump_age
21from ..http import dump_cookie
22from ..http import dump_header
23from ..http import dump_options_header
24from ..http import http_date
25from ..http import parse_age
26from ..http import parse_date
27from ..http import parse_options_header
28from ..http import quote_etag
29from ..http import unquote_etag
30from ..utils import get_content_type
31from ..utils import header_property
32
33
34def _set_property(name: str, doc: str | None = None) -> property:
35 def fget(self: Response) -> HeaderSet:
36 def on_update(header_set: HeaderSet) -> None:
37 if not header_set and name in self.headers:
38 del self.headers[name]
39 elif header_set:
40 self.headers[name] = header_set.to_header()
41
42 obj = HeaderSet.from_header(self.headers.get(name))
43 obj._on_update = on_update
44 return obj
45
46 def fset(
47 self: Response,
48 value: None | (str | dict[str, str | int] | t.Iterable[str]),
49 ) -> None:
50 if not value:
51 del self.headers[name]
52 elif isinstance(value, str):
53 self.headers[name] = value
54 else:
55 self.headers[name] = dump_header(value)
56
57 return property(fget, fset, doc=doc)
58
59
60class Response:
61 """Represents the non-IO parts of an HTTP response, specifically the
62 status and headers but not the body.
63
64 This class is not meant for general use. It should only be used when
65 implementing WSGI, ASGI, or another HTTP application spec. Werkzeug
66 provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`.
67
68 :param status: The status code for the response. Either an int, in
69 which case the default status message is added, or a string in
70 the form ``{code} {message}``, like ``404 Not Found``. Defaults
71 to 200.
72 :param headers: A :class:`~werkzeug.datastructures.Headers` object,
73 or a list of ``(key, value)`` tuples that will be converted to a
74 ``Headers`` object.
75 :param mimetype: The mime type (content type without charset or
76 other parameters) of the response. If the value starts with
77 ``text/`` (or matches some other special cases), the charset
78 will be added to create the ``content_type``.
79 :param content_type: The full content type of the response.
80 Overrides building the value from ``mimetype``.
81
82 .. versionchanged:: 3.0
83 The ``charset`` attribute was removed.
84
85 .. versionadded:: 2.0
86 """
87
88 #: the default status if none is provided.
89 default_status = 200
90
91 #: the default mimetype if none is provided.
92 default_mimetype: str | None = "text/plain"
93
94 #: Warn if a cookie header exceeds this size. The default, 4093, should be
95 #: safely `supported by most browsers <cookie_>`_. A cookie larger than
96 #: this size will still be sent, but it may be ignored or handled
97 #: incorrectly by some browsers. Set to 0 to disable this check.
98 #:
99 #: .. versionadded:: 0.13
100 #:
101 #: .. _`cookie`: http://browsercookielimits.squawky.net/
102 max_cookie_size = 4093
103
104 # A :class:`Headers` object representing the response headers.
105 headers: Headers
106
107 def __init__(
108 self,
109 status: int | str | HTTPStatus | None = None,
110 headers: t.Mapping[str, str | t.Iterable[str]]
111 | t.Iterable[tuple[str, str]]
112 | None = None,
113 mimetype: str | None = None,
114 content_type: str | None = None,
115 ) -> None:
116 if isinstance(headers, Headers):
117 self.headers = headers
118 elif not headers:
119 self.headers = Headers()
120 else:
121 self.headers = Headers(headers)
122
123 if content_type is None:
124 if mimetype is None and "Content-Type" not in self.headers:
125 mimetype = self.default_mimetype
126 if mimetype is not None:
127 mimetype = get_content_type(mimetype, "utf-8")
128 content_type = mimetype
129 if content_type is not None:
130 self.headers["Content-Type"] = content_type
131 if status is None:
132 status = self.default_status
133 self.status = status
134
135 def __repr__(self) -> str:
136 return f"<{type(self).__name__} [{self.status}]>"
137
138 @property
139 def status_code(self) -> int:
140 """The HTTP status code as a number."""
141 return self._status_code
142
143 @status_code.setter
144 def status_code(self, code: int) -> None:
145 self.status = code
146
147 @property
148 def status(self) -> str:
149 """The HTTP status code as a string."""
150 return self._status
151
152 @status.setter
153 def status(self, value: str | int | HTTPStatus) -> None:
154 self._status, self._status_code = self._clean_status(value)
155
156 def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]:
157 if isinstance(value, (int, HTTPStatus)):
158 status_code = int(value)
159 else:
160 value = value.strip()
161
162 if not value:
163 raise ValueError("Empty status argument")
164
165 code_str, sep, _ = value.partition(" ")
166
167 try:
168 status_code = int(code_str)
169 except ValueError:
170 # only message
171 return f"0 {value}", 0
172
173 if sep:
174 # code and message
175 return value, status_code
176
177 # only code, look up message
178 try:
179 status = f"{status_code} {HTTPStatus(status_code).phrase}"
180 except ValueError:
181 status = f"{status_code} Unknown"
182
183 return status, status_code
184
185 def set_cookie(
186 self,
187 key: str,
188 value: str = "",
189 max_age: timedelta | int | None = None,
190 expires: str | datetime | int | float | None = None,
191 path: str | None = "/",
192 domain: str | None = None,
193 secure: bool = False,
194 httponly: bool = False,
195 samesite: str | None = None,
196 partitioned: bool = False,
197 ) -> None:
198 """Sets a cookie.
199
200 A warning is raised if the size of the cookie header exceeds
201 :attr:`max_cookie_size`, but the header will still be set.
202
203 :param key: the key (name) of the cookie to be set.
204 :param value: the value of the cookie.
205 :param max_age: should be a number of seconds, or `None` (default) if
206 the cookie should last only as long as the client's
207 browser session.
208 :param expires: should be a `datetime` object or UNIX timestamp.
209 :param path: limits the cookie to a given path, per default it will
210 span the whole domain.
211 :param domain: if you want to set a cross-domain cookie. For example,
212 ``domain="example.com"`` will set a cookie that is
213 readable by the domain ``www.example.com``,
214 ``foo.example.com`` etc. Otherwise, a cookie will only
215 be readable by the domain that set it.
216 :param secure: If ``True``, the cookie will only be available
217 via HTTPS.
218 :param httponly: Disallow JavaScript access to the cookie.
219 :param samesite: Limit the scope of the cookie to only be
220 attached to requests that are "same-site".
221 :param partitioned: If ``True``, the cookie will be partitioned.
222
223 .. versionchanged:: 3.1
224 The ``partitioned`` parameter was added.
225 """
226 self.headers.add(
227 "Set-Cookie",
228 dump_cookie(
229 key,
230 value=value,
231 max_age=max_age,
232 expires=expires,
233 path=path,
234 domain=domain,
235 secure=secure,
236 httponly=httponly,
237 max_size=self.max_cookie_size,
238 samesite=samesite,
239 partitioned=partitioned,
240 ),
241 )
242
243 def delete_cookie(
244 self,
245 key: str,
246 path: str | None = "/",
247 domain: str | None = None,
248 secure: bool = False,
249 httponly: bool = False,
250 samesite: str | None = None,
251 partitioned: bool = False,
252 ) -> None:
253 """Delete a cookie. Fails silently if key doesn't exist.
254
255 :param key: the key (name) of the cookie to be deleted.
256 :param path: if the cookie that should be deleted was limited to a
257 path, the path has to be defined here.
258 :param domain: if the cookie that should be deleted was limited to a
259 domain, that domain has to be defined here.
260 :param secure: If ``True``, the cookie will only be available
261 via HTTPS.
262 :param httponly: Disallow JavaScript access to the cookie.
263 :param samesite: Limit the scope of the cookie to only be
264 attached to requests that are "same-site".
265 :param partitioned: If ``True``, the cookie will be partitioned.
266 """
267 self.set_cookie(
268 key,
269 expires=0,
270 max_age=0,
271 path=path,
272 domain=domain,
273 secure=secure,
274 httponly=httponly,
275 samesite=samesite,
276 partitioned=partitioned,
277 )
278
279 @property
280 def is_json(self) -> bool:
281 """Check if the mimetype indicates JSON data, either
282 :mimetype:`application/json` or :mimetype:`application/*+json`.
283 """
284 mt = self.mimetype
285 return mt is not None and (
286 mt == "application/json"
287 or mt.startswith("application/")
288 and mt.endswith("+json")
289 )
290
291 # Common Descriptors
292
293 @property
294 def mimetype(self) -> str | None:
295 """The mimetype (content type without charset etc.)"""
296 ct = self.headers.get("Content-Type")
297
298 if ct:
299 return ct.split(";")[0].strip()
300 else:
301 return None
302
303 @mimetype.setter
304 def mimetype(self, value: str) -> None:
305 self.headers["Content-Type"] = get_content_type(value, "utf-8")
306
307 @property
308 def mimetype_params(self) -> dict[str, str]:
309 """The mimetype parameters as dict. For example if the
310 content type is ``text/html; charset=utf-8`` the params would be
311 ``{'charset': 'utf-8'}``.
312
313 .. versionadded:: 0.5
314 """
315
316 def on_update(d: CallbackDict[str, str]) -> None:
317 self.headers["Content-Type"] = dump_options_header(self.mimetype, d)
318
319 d = parse_options_header(self.headers.get("Content-Type", ""))[1]
320 return CallbackDict(d, on_update)
321
322 location = header_property[str](
323 "Location",
324 doc="""The Location response-header field is used to redirect
325 the recipient to a location other than the Request-URI for
326 completion of the request or identification of a new
327 resource.""",
328 )
329 age = header_property(
330 "Age",
331 None,
332 parse_age,
333 dump_age, # type: ignore
334 doc="""The Age response-header field conveys the sender's
335 estimate of the amount of time since the response (or its
336 revalidation) was generated at the origin server.
337
338 Age values are non-negative decimal integers, representing time
339 in seconds.""",
340 )
341 content_type = header_property[str](
342 "Content-Type",
343 doc="""The Content-Type entity-header field indicates the media
344 type of the entity-body sent to the recipient or, in the case of
345 the HEAD method, the media type that would have been sent had
346 the request been a GET.""",
347 )
348 content_length = header_property(
349 "Content-Length",
350 None,
351 int,
352 str,
353 doc="""The Content-Length entity-header field indicates the size
354 of the entity-body, in decimal number of OCTETs, sent to the
355 recipient or, in the case of the HEAD method, the size of the
356 entity-body that would have been sent had the request been a
357 GET.""",
358 )
359 content_location = header_property[str](
360 "Content-Location",
361 doc="""The Content-Location entity-header field MAY be used to
362 supply the resource location for the entity enclosed in the
363 message when that entity is accessible from a location separate
364 from the requested resource's URI.""",
365 )
366 content_encoding = header_property[str](
367 "Content-Encoding",
368 doc="""The Content-Encoding entity-header field is used as a
369 modifier to the media-type. When present, its value indicates
370 what additional content codings have been applied to the
371 entity-body, and thus what decoding mechanisms must be applied
372 in order to obtain the media-type referenced by the Content-Type
373 header field.""",
374 )
375
376 @property
377 def content_md5(self) -> str | None:
378 """The ``Content-MD5`` header, an MD5 digest of the response body.
379
380 .. deprecated:: 3.2
381 The header has not been used for a long time. Will be removed
382 in Werkzeug 3.3.
383 """
384 import warnings
385
386 warnings.warn(
387 "The 'content_md5' attribute is deprecated and will be removed in"
388 " Werkzeug 3.3. The header has not been used for a long time.",
389 DeprecationWarning,
390 stacklevel=2,
391 )
392 return self.headers.get("Content-MD5")
393
394 @content_md5.setter
395 def content_md5(self, value: str | None) -> None:
396 import warnings
397
398 warnings.warn(
399 "The 'content_md5' attribute is deprecated and will be removed in"
400 " Werkzeug 3.3. The header has not been used for a long time.",
401 DeprecationWarning,
402 stacklevel=2,
403 )
404
405 if value is None:
406 del self.headers["Content-MD5"]
407 else:
408 self.headers["Content-MD5"] = value
409
410 @content_md5.deleter
411 def content_md5(self) -> None:
412 import warnings
413
414 warnings.warn(
415 "The 'content_md5' attribute is deprecated and will be removed in"
416 " Werkzeug 3.3. The header has not been used for a long time.",
417 DeprecationWarning,
418 stacklevel=2,
419 )
420 del self.headers["Content-MD5"]
421
422 date = header_property(
423 "Date",
424 None,
425 parse_date,
426 http_date,
427 doc="""The Date general-header field represents the date and
428 time at which the message was originated, having the same
429 semantics as orig-date in RFC 822.
430
431 .. versionchanged:: 2.0
432 The datetime object is timezone-aware.
433 """,
434 )
435 expires = header_property(
436 "Expires",
437 None,
438 parse_date,
439 http_date,
440 doc="""The Expires entity-header field gives the date/time after
441 which the response is considered stale. A stale cache entry may
442 not normally be returned by a cache.
443
444 .. versionchanged:: 2.0
445 The datetime object is timezone-aware.
446 """,
447 )
448 last_modified = header_property(
449 "Last-Modified",
450 None,
451 parse_date,
452 http_date,
453 doc="""The Last-Modified entity-header field indicates the date
454 and time at which the origin server believes the variant was
455 last modified.
456
457 .. versionchanged:: 2.0
458 The datetime object is timezone-aware.
459 """,
460 )
461
462 @property
463 def retry_after(self) -> datetime | None:
464 """The Retry-After response-header field can be used with a
465 503 (Service Unavailable) response to indicate how long the
466 service is expected to be unavailable to the requesting client.
467
468 Time in seconds until expiration or date.
469
470 .. versionchanged:: 2.0
471 The datetime object is timezone-aware.
472 """
473 value = self.headers.get("Retry-After")
474 if value is None:
475 return None
476
477 try:
478 seconds = int(value)
479 except ValueError:
480 return parse_date(value)
481
482 return datetime.now(timezone.utc) + timedelta(seconds=seconds)
483
484 @retry_after.setter
485 def retry_after(self, value: datetime | int | str | None) -> None:
486 if value is None:
487 if "Retry-After" in self.headers:
488 del self.headers["Retry-After"]
489 return
490 elif isinstance(value, datetime):
491 value = http_date(value)
492 else:
493 value = str(value)
494 self.headers["Retry-After"] = value
495
496 vary = _set_property(
497 "Vary",
498 doc="""The Vary field value indicates the set of request-header
499 fields that fully determines, while the response is fresh,
500 whether a cache is permitted to use the response to reply to a
501 subsequent request without revalidation.""",
502 )
503 content_language = _set_property(
504 "Content-Language",
505 doc="""The Content-Language entity-header field describes the
506 natural language(s) of the intended audience for the enclosed
507 entity. Note that this might not be equivalent to all the
508 languages used within the entity-body.""",
509 )
510 allow = _set_property(
511 "Allow",
512 doc="""The Allow entity-header field lists the set of methods
513 supported by the resource identified by the Request-URI. The
514 purpose of this field is strictly to inform the recipient of
515 valid methods associated with the resource. An Allow header
516 field MUST be present in a 405 (Method Not Allowed)
517 response.""",
518 )
519
520 # ETag
521
522 @property
523 def cache_control(self) -> ResponseCacheControl:
524 """The Cache-Control general-header field is used to specify
525 directives that MUST be obeyed by all caching mechanisms along the
526 request/response chain.
527 """
528
529 def on_update(cache_control: _CacheControl) -> None:
530 if not cache_control and "Cache-Control" in self.headers:
531 del self.headers["Cache-Control"]
532 elif cache_control:
533 self.headers["Cache-Control"] = cache_control.to_header()
534
535 obj = ResponseCacheControl.from_header(self.headers.get("Cache-Control"))
536 obj.on_update = on_update
537 return obj
538
539 def set_etag(self, etag: str, weak: bool = False) -> None:
540 """Set the etag, and override the old one if there was one."""
541 self.headers["ETag"] = quote_etag(etag, weak)
542
543 def get_etag(self) -> tuple[str, bool] | tuple[None, None]:
544 """Return a tuple in the form ``(etag, is_weak)``. If there is no
545 ETag the return value is ``(None, None)``.
546 """
547 return unquote_etag(self.headers.get("ETag"))
548
549 accept_ranges = header_property[str](
550 "Accept-Ranges",
551 doc="""The `Accept-Ranges` header. Even though the name would
552 indicate that multiple values are supported, it must be one
553 string token only.
554
555 The values ``'bytes'`` and ``'none'`` are common.
556
557 .. versionadded:: 0.7""",
558 )
559
560 @property
561 def content_range(self) -> ContentRange:
562 """The ``Content-Range`` header as a
563 :class:`~werkzeug.datastructures.ContentRange` object. Available
564 even if the header is not set.
565
566 .. versionadded:: 0.7
567 """
568
569 def on_update(rng: ContentRange) -> None:
570 if not rng:
571 del self.headers["Content-Range"]
572 else:
573 self.headers["Content-Range"] = rng.to_header()
574
575 obj = ContentRange.from_header(self.headers.get("Content-Range"))
576 # always provide a content range object to make the descriptor
577 # more user friendly. It provides an unset() method that can be
578 # used to remove the header quickly.
579 if obj is None:
580 obj = ContentRange(None, None, None)
581
582 obj._on_update = on_update
583 return obj
584
585 @content_range.setter
586 def content_range(self, value: ContentRange | str | None) -> None:
587 if not value:
588 del self.headers["Content-Range"]
589 elif isinstance(value, str):
590 self.headers["Content-Range"] = value
591 else:
592 self.headers["Content-Range"] = value.to_header()
593
594 # Authorization
595
596 @property
597 def www_authenticate(self) -> WWWAuthenticate:
598 """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate`
599 object. Modifying the object will modify the header value.
600
601 This header is not set by default. To set this header, assign an instance of
602 :class:`.WWWAuthenticate` to this attribute.
603
604 .. code-block:: python
605
606 response.www_authenticate = WWWAuthenticate(
607 "basic", {"realm": "Authentication Required"}
608 )
609
610 Multiple values for this header can be sent to give the client multiple options.
611 Assign a list to set multiple headers. However, modifying the items in the list
612 will not automatically update the header values, and accessing this attribute
613 will only ever return the first value.
614
615 To unset this header, assign ``None`` or use ``del``.
616
617 .. versionchanged:: 2.3
618 This attribute can be assigned to set the header. A list can be assigned
619 to set multiple header values. Use ``del`` to unset the header.
620
621 .. versionchanged:: 2.3
622 :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute
623 was added for auth challenges that use a token instead of parameters.
624 """
625 value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate"))
626
627 if value is None:
628 value = WWWAuthenticate("basic")
629
630 def on_update(value: WWWAuthenticate) -> None:
631 self.www_authenticate = value
632
633 value._on_update = on_update
634 return value
635
636 @www_authenticate.setter
637 def www_authenticate(
638 self, value: WWWAuthenticate | list[WWWAuthenticate] | None
639 ) -> None:
640 if not value: # None or empty list
641 del self.www_authenticate
642 elif isinstance(value, list):
643 # Clear any existing header by setting the first item.
644 self.headers.set("WWW-Authenticate", value[0].to_header())
645
646 for item in value[1:]:
647 # Add additional header lines for additional items.
648 self.headers.add("WWW-Authenticate", item.to_header())
649 else:
650 self.headers.set("WWW-Authenticate", value.to_header())
651
652 def on_update(value: WWWAuthenticate) -> None:
653 self.www_authenticate = value
654
655 # When setting a single value, allow updating it directly.
656 value._on_update = on_update
657
658 @www_authenticate.deleter
659 def www_authenticate(self) -> None:
660 if "WWW-Authenticate" in self.headers:
661 del self.headers["WWW-Authenticate"]
662
663 # CSP
664
665 @property
666 def content_security_policy(self) -> ContentSecurityPolicy:
667 """The ``Content-Security-Policy`` header as a
668 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available
669 even if the header is not set.
670
671 The Content-Security-Policy header adds an additional layer of
672 security to help detect and mitigate certain types of attacks.
673 """
674
675 def on_update(csp: ContentSecurityPolicy) -> None:
676 if not csp:
677 del self.headers["Content-Security-Policy"]
678 else:
679 self.headers["Content-Security-Policy"] = csp.to_header()
680
681 obj = ContentSecurityPolicy.from_header(
682 self.headers.get("Content-Security-Policy")
683 )
684 obj.on_update = on_update
685 return obj
686
687 @content_security_policy.setter
688 def content_security_policy(
689 self, value: ContentSecurityPolicy | str | None
690 ) -> None:
691 if not value:
692 del self.headers["Content-Security-Policy"]
693 elif isinstance(value, str):
694 self.headers["Content-Security-Policy"] = value
695 else:
696 self.headers["Content-Security-Policy"] = value.to_header()
697
698 @property
699 def content_security_policy_report_only(self) -> ContentSecurityPolicy:
700 """The ``Content-Security-Policy-Report-Only`` header as a
701 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available
702 even if the header is not set.
703
704 The Content-Security-Policy-Report-Only header adds a csp policy
705 that is not enforced but is reported thereby helping detect
706 certain types of attacks.
707 """
708
709 def on_update(csp: ContentSecurityPolicy) -> None:
710 if not csp:
711 del self.headers["Content-Security-Policy-Report-Only"]
712 else:
713 self.headers["Content-Security-Policy-Report-Only"] = csp.to_header()
714
715 obj = ContentSecurityPolicy.from_header(
716 self.headers.get("Content-Security-Policy-Report-Only")
717 )
718 obj.on_update = on_update
719 return obj
720
721 @content_security_policy_report_only.setter
722 def content_security_policy_report_only(
723 self, value: ContentSecurityPolicy | str | None
724 ) -> None:
725 if not value:
726 del self.headers["Content-Security-Policy-Report-Only"]
727 elif isinstance(value, str):
728 self.headers["Content-Security-Policy-Report-Only"] = value
729 else:
730 self.headers["Content-Security-Policy-Report-Only"] = value.to_header()
731
732 # CORS
733
734 @property
735 def access_control_allow_credentials(self) -> bool:
736 """Whether credentials can be shared by the browser to
737 JavaScript code. As part of the preflight request it indicates
738 whether credentials can be used on the cross origin request.
739 """
740 return "Access-Control-Allow-Credentials" in self.headers
741
742 @access_control_allow_credentials.setter
743 def access_control_allow_credentials(self, value: bool | None) -> None:
744 if value is True:
745 self.headers["Access-Control-Allow-Credentials"] = "true"
746 else:
747 self.headers.pop("Access-Control-Allow-Credentials", None)
748
749 access_control_allow_headers = header_property[HeaderSet](
750 "Access-Control-Allow-Headers",
751 load_func=HeaderSet.from_header,
752 dump_func=dump_header,
753 doc="Which headers can be sent with the cross origin request.",
754 )
755
756 access_control_allow_methods = header_property[HeaderSet](
757 "Access-Control-Allow-Methods",
758 load_func=HeaderSet.from_header,
759 dump_func=dump_header,
760 doc="Which methods can be used for the cross origin request.",
761 )
762
763 access_control_allow_origin = header_property[str](
764 "Access-Control-Allow-Origin",
765 doc="The origin or '*' for any origin that may make cross origin requests.",
766 )
767
768 access_control_expose_headers = header_property[HeaderSet](
769 "Access-Control-Expose-Headers",
770 load_func=HeaderSet.from_header,
771 dump_func=dump_header,
772 doc="Which headers can be shared by the browser to JavaScript code.",
773 )
774
775 access_control_max_age = header_property(
776 "Access-Control-Max-Age",
777 load_func=int,
778 dump_func=str,
779 doc="The maximum age in seconds the access control settings can be cached for.",
780 )
781
782 cross_origin_opener_policy = header_property[COOP](
783 "Cross-Origin-Opener-Policy",
784 load_func=COOP,
785 dump_func=lambda value: value.value,
786 default=COOP.UNSAFE_NONE,
787 doc="""Allows control over sharing of browsing context group with cross-origin
788 documents.
789
790 Values are members of the :class:`.COOP` enum.
791
792 .. versionadded:: 2.0
793 """,
794 )
795
796 cross_origin_embedder_policy = header_property[COEP](
797 "Cross-Origin-Embedder-Policy",
798 load_func=COEP,
799 dump_func=lambda value: value.value,
800 default=COEP.UNSAFE_NONE,
801 doc="""Prevents a document from loading any cross-origin resources that do not
802 explicitly grant the document permission.
803
804 Values are members of the :class:`.COEP` enum.
805
806 .. versionadded:: 2.0
807 """,
808 )
809
810 cross_origin_resource_policy = header_property[CORP](
811 "Cross-Origin-Resource-Policy",
812 load_func=CORP,
813 dump_func=lambda value: value.value,
814 doc="""specifies the policy for what sites/origins should be allowed to load
815 this resource.
816
817 Values are members of the :class:`.CORP` enum.
818
819 .. versionadded:: 3.2
820 """,
821 )