Home Assistant Unofficial Reference 2024.12.1
http.py
Go to the documentation of this file.
1 """Helper to track the current http request."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable
7 from contextvars import ContextVar
8 from http import HTTPStatus
9 import logging
10 from typing import Any, Final
11 
12 from aiohttp import web
13 from aiohttp.typedefs import LooseHeaders
14 from aiohttp.web import AppKey, Request
15 from aiohttp.web_exceptions import (
16  HTTPBadRequest,
17  HTTPInternalServerError,
18  HTTPUnauthorized,
19 )
20 from aiohttp.web_urldispatcher import AbstractResource, AbstractRoute
21 import voluptuous as vol
22 
23 from homeassistant import exceptions
24 from homeassistant.const import CONTENT_TYPE_JSON
25 from homeassistant.core import Context, HomeAssistant, is_callback
26 from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data
27 
28 from .json import find_paths_unserializable_data, json_bytes, json_dumps
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 
33 type AllowCorsType = Callable[[AbstractRoute | AbstractResource], None]
34 KEY_AUTHENTICATED: Final = "ha_authenticated"
35 KEY_ALLOW_ALL_CORS = AppKey[AllowCorsType]("allow_all_cors")
36 KEY_ALLOW_CONFIGURED_CORS = AppKey[AllowCorsType]("allow_configured_cors")
37 KEY_HASS: AppKey[HomeAssistant] = AppKey("hass")
38 
39 current_request: ContextVar[Request | None] = ContextVar(
40  "current_request", default=None
41 )
42 
43 
45  hass: HomeAssistant, view: HomeAssistantView, handler: Callable
46 ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
47  """Wrap the handler classes."""
48  is_coroutinefunction = asyncio.iscoroutinefunction(handler)
49  assert is_coroutinefunction or is_callback(
50  handler
51  ), "Handler should be a coroutine or a callback."
52 
53  async def handle(request: web.Request) -> web.StreamResponse:
54  """Handle incoming request."""
55  if hass.is_stopping:
56  return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE)
57 
58  authenticated = request.get(KEY_AUTHENTICATED, False)
59 
60  if view.requires_auth and not authenticated:
61  raise HTTPUnauthorized
62 
63  if _LOGGER.isEnabledFor(logging.DEBUG):
64  _LOGGER.debug(
65  "Serving %s to %s (auth: %s)",
66  request.path,
67  request.remote,
68  authenticated,
69  )
70 
71  try:
72  if is_coroutinefunction:
73  result = await handler(request, **request.match_info)
74  else:
75  result = handler(request, **request.match_info)
76  except vol.Invalid as err:
77  raise HTTPBadRequest from err
78  except exceptions.ServiceNotFound as err:
79  raise HTTPInternalServerError from err
80  except exceptions.Unauthorized as err:
81  raise HTTPUnauthorized from err
82 
83  if isinstance(result, web.StreamResponse):
84  # The method handler returned a ready-made Response, how nice of it
85  return result
86 
87  status_code = HTTPStatus.OK
88  if isinstance(result, tuple):
89  result, status_code = result
90 
91  if isinstance(result, bytes):
92  return web.Response(body=result, status=status_code)
93 
94  if isinstance(result, str):
95  return web.Response(text=result, status=status_code)
96 
97  if result is None:
98  return web.Response(body=b"", status=status_code)
99 
100  raise TypeError(
101  f"Result should be None, string, bytes or StreamResponse. Got: {result}"
102  )
103 
104  return handle
105 
106 
108  """Base view for all views."""
109 
110  url: str | None = None
111  extra_urls: list[str] = []
112  # Views inheriting from this class can override this
113  requires_auth = True
114  cors_allowed = False
115 
116  @staticmethod
117  def context(request: web.Request) -> Context:
118  """Generate a context from a request."""
119  if (user := request.get("hass_user")) is None:
120  return Context()
121 
122  return Context(user_id=user.id)
123 
124  @staticmethod
125  def json(
126  result: Any,
127  status_code: HTTPStatus | int = HTTPStatus.OK,
128  headers: LooseHeaders | None = None,
129  ) -> web.Response:
130  """Return a JSON response."""
131  try:
132  msg = json_bytes(result)
133  except JSON_ENCODE_EXCEPTIONS as err:
134  _LOGGER.error(
135  "Unable to serialize to JSON. Bad data found at %s",
137  find_paths_unserializable_data(result, dump=json_dumps)
138  ),
139  )
140  raise HTTPInternalServerError from err
141  response = web.Response(
142  body=msg,
143  content_type=CONTENT_TYPE_JSON,
144  status=int(status_code),
145  headers=headers,
146  zlib_executor_size=32768,
147  )
148  response.enable_compression()
149  return response
150 
152  self,
153  message: str,
154  status_code: HTTPStatus | int = HTTPStatus.OK,
155  message_code: str | None = None,
156  headers: LooseHeaders | None = None,
157  ) -> web.Response:
158  """Return a JSON message response."""
159  data = {"message": message}
160  if message_code is not None:
161  data["code"] = message_code
162  return self.jsonjson(data, status_code, headers=headers)
163 
164  def register(
165  self, hass: HomeAssistant, app: web.Application, router: web.UrlDispatcher
166  ) -> None:
167  """Register the view with a router."""
168  assert self.url is not None, "No url set for view"
169  urls = [self.url, *self.extra_urls]
170  routes: list[AbstractRoute] = []
171 
172  for method in ("get", "post", "delete", "put", "patch", "head", "options"):
173  if not (handler := getattr(self, method, None)):
174  continue
175 
176  handler = request_handler_factory(hass, self, handler)
177 
178  routes.extend(router.add_route(method, url, handler) for url in urls)
179 
180  # Use `get` because CORS middleware is not be loaded in emulated_hue
181  if self.cors_allowedcors_allowed:
182  allow_cors = app.get(KEY_ALLOW_ALL_CORS)
183  else:
184  allow_cors = app.get(KEY_ALLOW_CONFIGURED_CORS)
185 
186  if allow_cors:
187  for route in routes:
188  allow_cors(route)
None register(self, HomeAssistant hass, web.Application app, web.UrlDispatcher router)
Definition: http.py:166
web.Response json(Any result, HTTPStatus|int status_code=HTTPStatus.OK, LooseHeaders|None headers=None)
Definition: http.py:129
web.Response json_message(self, str message, HTTPStatus|int status_code=HTTPStatus.OK, str|None message_code=None, LooseHeaders|None headers=None)
Definition: http.py:157
Context context(web.Request request)
Definition: http.py:117
bool is_callback(Callable[..., Any] func)
Definition: core.py:259
Callable[[web.Request], Awaitable[web.StreamResponse]] request_handler_factory(HomeAssistant hass, HomeAssistantView view, Callable handler)
Definition: http.py:46
dict[str, Any] find_paths_unserializable_data(Any bad_data, *Callable[[Any], str] dump=json.dumps)
Definition: json.py:233
str format_unserializable_data(dict[str, Any] data)
Definition: json.py:126