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

1"""WSGI application for the web server.""" 

2 

3import gws 

4import gws.base.web 

5import gws.config 

6 

7_STATE = { 

8 'inited': False, 

9} 

10 

11 

12def application(environ, start_response): 

13 """WSGI application, loads the configuration on the first call. 

14 

15 Args: 

16 environ: WSGI environment. 

17 start_response: WSGI ``start_response`` function. 

18 

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) 

27 

28 

29def make_application(root): 

30 """Create a WSGI application for a given object tree root, without loading the configuration. 

31 

32 Args: 

33 root: Object tree root. 

34 

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) 

41 

42 return fn 

43 

44 

45def init(): 

46 """Load the configuration and set the log level. 

47 

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) 

59 

60 

61def reload(): 

62 """Reload the configuration.""" 

63 _STATE['inited'] = False 

64 init() 

65 

66 

67def handle_request(root: gws.Root, environ) -> gws.WebResponder: 

68 """Handle a web request. 

69 

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. 

72 

73 Args: 

74 root: Object tree root. 

75 environ: WSGI environment. 

76 

77 Returns: 

78 A responder. 

79 """ 

80 req = gws.base.web.wsgi.Requester(root, environ, root.app.webMgr.site) 

81 

82 try: 

83 req.parse() 

84 except Exception as exc: 

85 return handle_error(req, exc) 

86 

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) 

92 

93 return res 

94 

95 

96def apply_middleware(root: gws.Root, req: gws.WebRequester) -> gws.WebResponder: 

97 """Run the middleware and the action for a request. 

98 

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. 

103 

104 Args: 

105 root: Object tree root. 

106 req: Web requester. 

107 

108 Returns: 

109 A responder. 

110 """ 

111 res = None 

112 done = [] 

113 

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) 

120 

121 if res: 

122 break 

123 

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) 

135 

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) 

141 

142 return res 

143 

144 

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]' 

153 

154 

155def handle_error(req: gws.WebRequester, exc: Exception) -> gws.WebResponder: 

156 """Create an error response for an exception. 

157 

158 Args: 

159 req: Web requester. 

160 exc: An exception, converted to an HTTP exception. 

161 

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) 

168 

169 

170def handle_http_error(req: gws.WebRequester, exc: gws.base.web.error.HTTPException) -> gws.WebResponder: 

171 """Create an error response for an HTTP exception. 

172 

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. 

176 

177 Args: 

178 req: Web requester. 

179 exc: HTTP exception. 

180 

181 Returns: 

182 A responder. 

183 """ 

184 # 

185 # @TODO: image errors 

186 

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 ) 

197 

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) 

203 

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) 

208 

209 

210_relaxed_read_options = { 

211 gws.SpecReadOption.caseInsensitive, 

212 gws.SpecReadOption.convertValues, 

213 gws.SpecReadOption.ignoreExtraProps, 

214} 

215 

216 

217def handle_action(root: gws.Root, req: gws.WebRequester) -> gws.WebResponder: 

218 """Run the action command for a request. 

219 

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. 

223 

224 Args: 

225 root: Object tree root. 

226 req: Web requester. 

227 

228 Returns: 

229 A content, redirect or API responder, depending on the type of the command response. 

230 

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') 

237 

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() 

253 

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 ) 

262 

263 response = fn(req, request) 

264 

265 if response is None: 

266 raise gws.NotFoundError(f'action not handled {category!r}:{req.command()!r}') 

267 

268 if isinstance(response, gws.ContentResponse): 

269 return req.content_responder(response) 

270 

271 if isinstance(response, gws.RedirectResponse): 

272 return req.redirect_responder(response) 

273 

274 return req.api_responder(response)