Coverage for gws-app/gws/base/web/wsgi_app.py: 68%
116 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""WSGI application for the web server."""
3import gws
4import gws.base.web
5import gws.config
7_STATE = {
8 'inited': False,
9}
12def application(environ, start_response):
13 """WSGI application, loads the configuration on the first call.
15 Args:
16 environ: WSGI environment.
17 start_response: WSGI ``start_response`` function.
19 Returns:
20 The WSGI response iterable.
21 """
22 if not _STATE['inited']:
23 init()
24 root = gws.get_root()
25 responder = handle_request(root, environ)
26 return responder.send_response(environ, start_response)
29def make_application(root):
30 """Create a WSGI application for a given object tree root, without loading the configuration.
32 Args:
33 root: Object tree root.
35 Returns:
36 A WSGI application function.
37 """
38 def fn(environ, start_response):
39 responder = handle_request(root, environ)
40 return responder.send_response(environ, start_response)
42 return fn
45def init():
46 """Load the configuration and set the log level.
48 Exits the process if the configuration cannot be loaded.
49 """
50 try:
51 gws.log.info('initializing WEB application')
52 gws.log.set_level('DEBUG')
53 root = gws.config.load()
54 gws.log.set_level(root.app.cfg('server.log.level'))
55 _STATE['inited'] = True
56 except:
57 gws.log.exception('UNABLE TO LOAD CONFIGURATION')
58 gws.u.exit(1)
61def reload():
62 """Reload the configuration."""
63 _STATE['inited'] = False
64 init()
67def handle_request(root: gws.Root, environ) -> gws.WebResponder:
68 """Handle a web request.
70 Creates a requester for the application web site, parses the request and runs
71 the middleware and the action. Errors are converted to error responses.
73 Args:
74 root: Object tree root.
75 environ: WSGI environment.
77 Returns:
78 A responder.
79 """
80 req = gws.base.web.wsgi.Requester(root, environ, root.app.webMgr.site)
82 try:
83 req.parse()
84 except Exception as exc:
85 return handle_error(req, exc)
87 gws.log.if_debug(_debug_repr, f'REQUEST_BEGIN <{req.method}> {req.command()}', req.params() or req.struct())
88 gws.debug.time_start(f'REQUEST <{req.method}> {req.command()}')
89 res = apply_middleware(root, req)
90 gws.debug.time_end()
91 gws.log.if_debug(_debug_repr, f'REQUEST_END <{req.method}> {req.command()}', res)
93 return res
96def apply_middleware(root: gws.Root, req: gws.WebRequester) -> gws.WebResponder:
97 """Run the middleware and the action for a request.
99 The ``enter_middleware`` methods are called in order until one of them returns a
100 response. If none does, GET and POST requests are passed to the action, HEAD and
101 OPTIONS requests get an empty response, other methods are not allowed. Then
102 ``exit_middleware`` is called, in reverse order, for each middleware that was entered.
104 Args:
105 root: Object tree root.
106 req: Web requester.
108 Returns:
109 A responder.
110 """
111 res = None
112 done = []
114 for obj in root.app.middlewareMgr.objects():
115 try:
116 res = obj.enter_middleware(req)
117 done.append(obj)
118 except Exception as exc:
119 res = handle_error(req, exc)
121 if res:
122 break
124 if not res:
125 try:
126 m = req.method
127 if m == gws.RequestMethod.GET or m == gws.RequestMethod.POST:
128 res = handle_action(root, req)
129 elif m == gws.RequestMethod.HEAD or m == gws.RequestMethod.OPTIONS:
130 res = req.content_responder(gws.ContentResponse(mimeType='text/plain', content=''))
131 else:
132 raise gws.base.web.error.MethodNotAllowed(['GET', 'POST', 'HEAD', 'OPTIONS'])
133 except Exception as exc:
134 res = handle_error(req, exc)
136 for obj in reversed(done):
137 try:
138 obj.exit_middleware(req, res)
139 except Exception as exc:
140 res = handle_error(req, exc)
142 return res
145def _debug_repr(prefix, s):
146 """Return a prefixed, truncated representation of an object for the debug log."""
147 s = repr(gws.u.to_dict(s))
148 m = 400
149 n = len(s)
150 if n <= m:
151 return prefix + ': ' + s
152 return prefix + ': ' + s[:m] + ' [...' + str(n - m) + ' more]'
155def handle_error(req: gws.WebRequester, exc: Exception) -> gws.WebResponder:
156 """Create an error response for an exception.
158 Args:
159 req: Web requester.
160 exc: An exception, converted to an HTTP exception.
162 Returns:
163 A responder.
164 """
165 gws.log.if_debug(_debug_repr, f'REQUEST_ERROR', exc)
166 web_exc = gws.base.web.error.from_exception(exc)
167 return handle_http_error(req, web_exc)
170def handle_http_error(req: gws.WebRequester, exc: gws.base.web.error.HTTPException) -> gws.WebResponder:
171 """Create an error response for an HTTP exception.
173 API requests get a structured response with the error code. Other requests get
174 the site error page or the ``application.error`` template, rendered with the
175 error code, or a plain error response if there is no template.
177 Args:
178 req: Web requester.
179 exc: HTTP exception.
181 Returns:
182 A responder.
183 """
184 #
185 # @TODO: image errors
187 if req.isApi:
188 return req.api_responder(
189 gws.Response(
190 status=exc.code,
191 error=gws.ResponseError(
192 code=exc.code,
193 info=gws.u.get(exc, 'description', ''),
194 ),
195 )
196 )
198 error_template = getattr(req.site, 'errorPage', None)
199 if not error_template:
200 error_template = req.root.app.templateMgr.find_template('application.error', where=[])
201 if not error_template:
202 return req.error_responder(exc)
204 args = gws.TemplateArgs(req=req, user=req.user, error=exc.code, status=exc.code or 500)
205 res = error_template.render(gws.TemplateRenderInput(args=args))
206 res.status = exc.code or 500
207 return req.content_responder(res)
210_relaxed_read_options = {
211 gws.SpecReadOption.caseInsensitive,
212 gws.SpecReadOption.convertValues,
213 gws.SpecReadOption.ignoreExtraProps,
214}
217def handle_action(root: gws.Root, req: gws.WebRequester) -> gws.WebResponder:
218 """Run the action command for a request.
220 API requests use the structured payload as parameters. GET and POST requests use
221 the GET parameters, which are read in a relaxed mode: case-insensitive, with value
222 conversion and extra parameters ignored.
224 Args:
225 root: Object tree root.
226 req: Web requester.
228 Returns:
229 A content, redirect or API responder, depending on the type of the command response.
231 Raises:
232 ``gws.NotFoundError``: If there is no command, or the command returns nothing.
233 ``gws.base.web.error.MethodNotAllowed``: If the request method is not supported.
234 """
235 if not req.command():
236 raise gws.NotFoundError('no command provided')
238 if req.isApi:
239 category = gws.CommandCategory.api
240 params = req.struct()
241 read_options = None
242 elif req.isGet:
243 category = gws.CommandCategory.get
244 params = req.params()
245 read_options = _relaxed_read_options
246 elif req.isPost:
247 category = gws.CommandCategory.post
248 params = req.params()
249 read_options = _relaxed_read_options
250 else:
251 # @TODO: add HEAD
252 raise gws.base.web.error.MethodNotAllowed()
254 fn, request = root.app.actionMgr.prepare_action(
255 category,
256 req.command(),
257 params,
258 req.path(),
259 req.user,
260 read_options,
261 )
263 response = fn(req, request)
265 if response is None:
266 raise gws.NotFoundError(f'action not handled {category!r}:{req.command()!r}')
268 if isinstance(response, gws.ContentResponse):
269 return req.content_responder(response)
271 if isinstance(response, gws.RedirectResponse):
272 return req.redirect_responder(response)
274 return req.api_responder(response)