Coverage for gws-app/gws/base/ows/server/error.py: 78%

72 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +0200

1"""OWS exceptions.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.lib.xmlx as xmlx 

7import gws.lib.image 

8import gws.lib.mime 

9 

10 

11class Error(gws.Error): 

12 """OWS error. 

13 

14 The class name is used as the OWS exception code. The HTTP status is 

15 derived from the code, 400 by default. 

16 """ 

17 

18 def __init__(self, *args): 

19 """Create an error. 

20 

21 Args: 

22 *args: The locator (usually the name of the offending parameter) and the error message, both optional. 

23 """ 

24 super().__init__(*args) 

25 self.code = self.__class__.__name__ 

26 self.locator = self.args[0] if len(self.args) > 0 else '' 

27 self.message = self.args[1] if len(self.args) > 1 else '' 

28 # NB assume it's the user's fault by default 

29 self.status = _STATUS.get(self.code, 400) 

30 

31 def to_xml_response(self, xmlns='ows') -> gws.ContentResponse: 

32 """Create an XML response for this error. 

33 

34 Args: 

35 xmlns: Format to use: ``ows`` for an OWS ``ExceptionReport``, 

36 ``ogc`` for an OGC ``ServiceExceptionReport``. 

37 

38 Returns: 

39 The XML response. 

40 

41 Raises: 

42 ``gws.Error``: If ``xmlns`` is invalid. 

43 """ 

44 

45 if xmlns == 'ows': 

46 # OWS ExceptionReport, as per OGC 06-121r9, 8.5 

47 ns = xmlx.namespace.c.OWS_11 

48 xml = xmlx.tag( 

49 'ExceptionReport', 

50 xmlx.tag( 

51 'Exception', 

52 {'exceptionCode': self.code, 'locator': self.locator}, 

53 self.message, 

54 ), 

55 ) 

56 

57 elif xmlns == 'ogc': 

58 # OGC ServiceExceptionReport, as per OGC 06-042, H.2 

59 ns = xmlx.namespace.c.OGC 

60 xml = xmlx.tag( 

61 'ServiceExceptionReport', 

62 xmlx.tag( 

63 'ServiceException', 

64 {'code': self.code}, 

65 self.message, 

66 ), 

67 ) 

68 

69 else: 

70 raise gws.Error(f'invalid {xmlns=}') 

71 

72 return gws.ContentResponse( 

73 status=self.status, 

74 mimeType=gws.lib.mime.XML, 

75 content=xml.to_string( 

76 gws.XmlOptions( 

77 defaultNamespace=ns, 

78 withXmlDeclaration=True, 

79 withNamespaceDeclarations=True, 

80 ) 

81 ), 

82 ) 

83 

84 def to_image_response(self, ifo: Optional[gws.ImageFormat] = None) -> gws.ContentResponse: 

85 """Create an image response for this error. 

86 

87 The image is a single error pixel. 

88 

89 Args: 

90 ifo: Image format, PNG24 by default. 

91 

92 Returns: 

93 The image response. 

94 """ 

95 

96 ifo = ifo or gws.lib.image.format.PNG24 

97 return gws.ContentResponse( 

98 status=self.status, 

99 mimeType=ifo.mimeTypes[0], 

100 content=gws.lib.image.util.error_pixel(ifo), 

101 ) 

102 

103 

104def from_exception(exc: Exception) -> Error: 

105 """Convert an exception to an OWS error. 

106 

107 ``gws.NotFoundError``, ``gws.ForbiddenError`` and ``gws.BadRequestError`` are 

108 mapped to the corresponding errors, any other exception is logged and becomes 

109 ``NoApplicableCode``. OWS errors are returned unchanged. 

110 

111 Args: 

112 exc: An exception. 

113 

114 Returns: 

115 The OWS error. A converted error has ``exc`` as its cause. 

116 """ 

117 

118 if isinstance(exc, Error): 

119 return exc 

120 

121 e = None 

122 

123 if isinstance(exc, gws.NotFoundError): 

124 e = NotFound() 

125 elif isinstance(exc, gws.ForbiddenError): 

126 e = Forbidden() 

127 elif isinstance(exc, gws.BadRequestError): 

128 e = BadRequest() 

129 

130 if e: 

131 gws.log.warning(f'OWS Exception: {e.code} cause={exc!r}') 

132 else: 

133 gws.log.exception() 

134 e = NoApplicableCode('', 'Internal Server Error') 

135 

136 e.__cause__ = exc 

137 return e 

138 

139 

140# @formatter:off 

141 

142# out extensions 

143 

144 

145class NotFound(Error): 

146 """The requested resource was not found (HTTP 404).""" 

147 

148 

149class Forbidden(Error): 

150 """Access to the requested resource is forbidden (HTTP 403).""" 

151 

152 

153class BadRequest(Error): 

154 """The request is invalid (HTTP 400).""" 

155 

156 

157# OGC 06-121r9 

158# Table 27 — Standard exception codes and meanings 

159 

160 

161class InvalidParameterValue(Error): 

162 """A parameter has an invalid value.""" 

163 

164 

165class InvalidUpdateSequence(Error): 

166 """The update sequence is greater than the current one.""" 

167 

168 

169class MissingParameterValue(Error): 

170 """A required parameter is missing.""" 

171 

172 

173class NoApplicableCode(Error): 

174 """No other exception code applies, e.g. an internal server error.""" 

175 

176 

177class OperationNotSupported(Error): 

178 """The requested operation is not supported.""" 

179 

180 

181class OptionNotSupported(Error): 

182 """The requested option is not supported.""" 

183 

184 

185class VersionNegotiationFailed(Error): 

186 """None of the requested versions is supported.""" 

187 

188 

189# OGC 06-042 

190# Table E.1 — Service exception codes 

191 

192 

193class CurrentUpdateSequence(Error): 

194 """The update sequence equals the current one.""" 

195 

196 

197class InvalidCRS(Error): 

198 """The requested CRS is not supported.""" 

199 

200 

201class InvalidDimensionValue(Error): 

202 """A dimension parameter has an invalid value.""" 

203 

204 

205class InvalidFormat(Error): 

206 """The requested format is not supported.""" 

207 

208 

209class InvalidPoint(Error): 

210 """The point coordinates are invalid.""" 

211 

212 

213class LayerNotDefined(Error): 

214 """The requested layer does not exist.""" 

215 

216 

217class LayerNotQueryable(Error): 

218 """The requested layer cannot be queried.""" 

219 

220 

221class MissingDimensionValue(Error): 

222 """A required dimension value is missing.""" 

223 

224 

225class StyleNotDefined(Error): 

226 """The requested style does not exist.""" 

227 

228 

229# OGC 07-057r7 

230# Table 20 — Exception codes for GetCapabilities operation 

231# Table 23 — Exception codes for GetTile operation 

232 

233 

234class PointIJOutOfRange(Error): 

235 """The pixel coordinates are outside the tile.""" 

236 

237 

238class TileOutOfRange(Error): 

239 """The tile row or column is out of range.""" 

240 

241 

242# OGC 09-025r1 

243# Table 3 — WFS exception codes 

244 

245 

246class CannotLockAllFeatures(Error): 

247 """Not all requested features could be locked.""" 

248 

249 

250class DuplicateStoredQueryIdValue(Error): 

251 """A stored query with this id already exists.""" 

252 

253 

254class DuplicateStoredQueryParameterName(Error): 

255 """A stored query parameter name is used more than once.""" 

256 

257 

258class FeaturesNotLocked(Error): 

259 """The features to modify are not locked.""" 

260 

261 

262class InvalidLockId(Error): 

263 """The lock id is invalid.""" 

264 

265 

266class InvalidValue(Error): 

267 """A value is invalid.""" 

268 

269 

270class LockHasExpired(Error): 

271 """The lock has expired.""" 

272 

273 

274class OperationParsingFailed(Error): 

275 """The request could not be parsed.""" 

276 

277 

278class OperationProcessingFailed(Error): 

279 """The request could not be processed.""" 

280 

281 

282class ResponseCacheExpired(Error): 

283 """The cached response has expired.""" 

284 

285 

286# OGC 06-121r9 8.6 HTTP STATUS codes for OGC Exceptions 

287 

288_STATUS = dict( 

289 OperationNotSupported=501, 

290 MissingParameterValue=400, 

291 InvalidParameterValue=400, 

292 VersionNegotiationFailed=400, 

293 InvalidUpdateSequence=400, 

294 OptionNotSupported=501, 

295 NoApplicableCode=500, 

296 NotFound=404, 

297 Forbidden=403, 

298 BadRequest=400, 

299)