penpot/backend/src/app/http/errors.clj
2023-02-02 13:38:04 +01:00

170 lines
5.9 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.http.errors
"A errors handling for the http server."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.session :as-alias session]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(defn- parse-client-ip
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(defn request->context
"Extracts error report relevant context data from request."
[request]
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:path (:path request)
:method (:method request)
:params (:params request)
:ip-addr (parse-client-ip request)
:user-agent (yrq/get-header request "user-agent")
:profile-id (:uid claims)
:version (or (yrq/get-header request "x-frontend-version")
"unknown")}))
(defmulti handle-exception
(fn [err & _rest]
(let [edata (ex-data err)]
(or (:type edata)
(class err)))))
(defmethod handle-exception :authentication
[err _]
(yrs/response 401 (ex-data err)))
(defmethod handle-exception :authorization
[err _]
(yrs/response 403 (ex-data err)))
(defmethod handle-exception :restriction
[err _]
(yrs/response 400 (ex-data err)))
(defmethod handle-exception :rate-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
(yrs/response :status 429 :body "" :headers headers)))
(defmethod handle-exception :validation
[err _]
(let [{:keys [code] :as data} (ex-data err)]
(cond
(= code :spec-validation)
(let [explain (ex/explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))))
(= code :request-body-too-large)
(yrs/response :status 413 :body data)
:else
(yrs/response :status 400 :body data))))
(defmethod handle-exception :assertion
[error request]
(let [edata (ex-data error)
explain (ex/explain edata)]
(binding [l/*context* (request->context request)]
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
(yrs/response :status 500
:body {:type :server-error
:code :assertion
:data (-> edata
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))}))))
(defmethod handle-exception :not-found
[err _]
(yrs/response 404 (ex-data err)))
(defmethod handle-exception :internal
[error request]
(let [{:keys [code] :as edata} (ex-data error)]
(cond
(= :concurrency-limit-reached code)
(yrs/response 429)
:else
(binding [l/*context* (request->context request)]
(l/error :hint "Internal error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
(let [state (.getSQLState ^java.sql.SQLException error)]
(binding [l/*context* (request->context request)]
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
(cond
(= state "57014")
(yrs/response 504 {:type :server-error
:code :statement-timeout
:hint (ex-message error)})
(= state "25P03")
(yrs/response 504 {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)})
:else
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state})))))
(defmethod handle-exception :default
[error request]
(let [edata (ex-data error)]
(cond
;; This means that exception is not a controlled exception.
(nil? edata)
(binding [l/*context* (request->context request)]
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)}))
;; This is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and
;; next-jdbc combines the two errors in a single ex-info. We
;; only need the :handling error, because the :rollback error
;; will be always "connection closed".
(and (ex/exception? (:rollback edata))
(ex/exception? (:handling edata)))
(handle-exception (:handling edata) request)
:else
(binding [l/*context* (request->context request)]
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(defn handle
[cause request]
(if (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(handle-exception (ex-cause cause) request)
(handle-exception cause request)))