debughelpers.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from __future__ import annotations
  2. import typing as t
  3. from jinja2.loaders import BaseLoader
  4. from werkzeug.routing import RequestRedirect
  5. from .blueprints import Blueprint
  6. from .globals import request_ctx
  7. from .sansio.app import App
  8. if t.TYPE_CHECKING:
  9. from .sansio.scaffold import Scaffold
  10. from .wrappers import Request
  11. class UnexpectedUnicodeError(AssertionError, UnicodeError):
  12. """Raised in places where we want some better error reporting for
  13. unexpected unicode or binary data.
  14. """
  15. class DebugFilesKeyError(KeyError, AssertionError):
  16. """Raised from request.files during debugging. The idea is that it can
  17. provide a better error message than just a generic KeyError/BadRequest.
  18. """
  19. def __init__(self, request: Request, key: str) -> None:
  20. form_matches = request.form.getlist(key)
  21. buf = [
  22. f"You tried to access the file {key!r} in the request.files"
  23. " dictionary but it does not exist. The mimetype for the"
  24. f" request is {request.mimetype!r} instead of"
  25. " 'multipart/form-data' which means that no file contents"
  26. " were transmitted. To fix this error you should provide"
  27. ' enctype="multipart/form-data" in your form.'
  28. ]
  29. if form_matches:
  30. names = ", ".join(repr(x) for x in form_matches)
  31. buf.append(
  32. "\n\nThe browser instead transmitted some file names. "
  33. f"This was submitted: {names}"
  34. )
  35. self.msg = "".join(buf)
  36. def __str__(self) -> str:
  37. return self.msg
  38. class FormDataRoutingRedirect(AssertionError):
  39. """This exception is raised in debug mode if a routing redirect
  40. would cause the browser to drop the method or body. This happens
  41. when method is not GET, HEAD or OPTIONS and the status code is not
  42. 307 or 308.
  43. """
  44. def __init__(self, request: Request) -> None:
  45. exc = request.routing_exception
  46. assert isinstance(exc, RequestRedirect)
  47. buf = [
  48. f"A request was sent to '{request.url}', but routing issued"
  49. f" a redirect to the canonical URL '{exc.new_url}'."
  50. ]
  51. if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
  52. buf.append(
  53. " The URL was defined with a trailing slash. Flask"
  54. " will redirect to the URL with a trailing slash if it"
  55. " was accessed without one."
  56. )
  57. buf.append(
  58. " Send requests to the canonical URL, or use 307 or 308 for"
  59. " routing redirects. Otherwise, browsers will drop form"
  60. " data.\n\n"
  61. "This exception is only raised in debug mode."
  62. )
  63. super().__init__("".join(buf))
  64. def attach_enctype_error_multidict(request: Request) -> None:
  65. """Patch ``request.files.__getitem__`` to raise a descriptive error
  66. about ``enctype=multipart/form-data``.
  67. :param request: The request to patch.
  68. :meta private:
  69. """
  70. oldcls = request.files.__class__
  71. class newcls(oldcls): # type: ignore[valid-type, misc]
  72. def __getitem__(self, key: str) -> t.Any:
  73. try:
  74. return super().__getitem__(key)
  75. except KeyError as e:
  76. if key not in request.form:
  77. raise
  78. raise DebugFilesKeyError(request, key).with_traceback(
  79. e.__traceback__
  80. ) from None
  81. newcls.__name__ = oldcls.__name__
  82. newcls.__module__ = oldcls.__module__
  83. request.files.__class__ = newcls
  84. def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]:
  85. yield f"class: {type(loader).__module__}.{type(loader).__name__}"
  86. for key, value in sorted(loader.__dict__.items()):
  87. if key.startswith("_"):
  88. continue
  89. if isinstance(value, (tuple, list)):
  90. if not all(isinstance(x, str) for x in value):
  91. continue
  92. yield f"{key}:"
  93. for item in value:
  94. yield f" - {item}"
  95. continue
  96. elif not isinstance(value, (str, int, float, bool)):
  97. continue
  98. yield f"{key}: {value!r}"
  99. def explain_template_loading_attempts(
  100. app: App,
  101. template: str,
  102. attempts: list[
  103. tuple[
  104. BaseLoader,
  105. Scaffold,
  106. tuple[str, str | None, t.Callable[[], bool] | None] | None,
  107. ]
  108. ],
  109. ) -> None:
  110. """This should help developers understand what failed"""
  111. info = [f"Locating template {template!r}:"]
  112. total_found = 0
  113. blueprint = None
  114. if request_ctx and request_ctx.request.blueprint is not None:
  115. blueprint = request_ctx.request.blueprint
  116. for idx, (loader, srcobj, triple) in enumerate(attempts):
  117. if isinstance(srcobj, App):
  118. src_info = f"application {srcobj.import_name!r}"
  119. elif isinstance(srcobj, Blueprint):
  120. src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"
  121. else:
  122. src_info = repr(srcobj)
  123. info.append(f"{idx + 1:5}: trying loader of {src_info}")
  124. for line in _dump_loader_info(loader):
  125. info.append(f" {line}")
  126. if triple is None:
  127. detail = "no match"
  128. else:
  129. detail = f"found ({triple[1] or '<string>'!r})"
  130. total_found += 1
  131. info.append(f" -> {detail}")
  132. seems_fishy = False
  133. if total_found == 0:
  134. info.append("Error: the template could not be found.")
  135. seems_fishy = True
  136. elif total_found > 1:
  137. info.append("Warning: multiple loaders returned a match for the template.")
  138. seems_fishy = True
  139. if blueprint is not None and seems_fishy:
  140. info.append(
  141. " The template was looked up from an endpoint that belongs"
  142. f" to the blueprint {blueprint!r}."
  143. )
  144. info.append(" Maybe you did not place a template in the right folder?")
  145. info.append(" See https://flask.palletsprojects.com/blueprints/#templates")
  146. app.logger.info("\n".join(info))