view pkg/pgxutils/errors.go @ 4441:940b53bdf871

Improved error message if gauge measurements reference no valid gauge.
author Sascha Wilde <wilde@intevation.de>
date Thu, 19 Sep 2019 18:20:19 +0200
parents bd97dc2dceea
children 49aa67fb2b6d
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2019 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Tom Gottfried <tom.gottfried@intevation.de>
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package pgxutils

import (
	"net/http"
	"strings"

	"github.com/jackc/pgx"
)

const (
	notNullViolation         = "23502"
	foreignKeyViolation      = "23503"
	uniqueViolation          = "23505"
	checkViolation           = "23514"
	violatesRowLevelSecurity = "42501"
	noDataFound              = "P0002"
)

// ReadableError wraps a given error Err and
// permits extraction of more user-friendly
// error messages from it in case it is an error
// from the PostgreSQL backend.
type ReadableError struct {
	Err error
}

func (re ReadableError) Error() string {
	m, _ := re.MessageAndCode()
	return m
}

// MessageAndCode returns a user-readable message
// and a matching HTTP status code.
// If its not a pgx.PgError it defaults to
// calling the parent Error method and returns its
// result together with http.StatusInternalServerError.
func (re ReadableError) MessageAndCode() (string, int) {
	if e, ok := re.Err.(pgx.PgError); ok {
		return messageAndCode(e)
	}
	return re.Err.Error(), http.StatusInternalServerError
}

func messageAndCode(err pgx.PgError) (m string, c int) {

	c = http.StatusInternalServerError

	switch err.Code {
	case notNullViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauges":
				switch err.ColumnName {
				case "objname":
					m = "Missing objname"
					return
				case "geom":
					m = "Missing lat/lon"
					return
				case "zero_point":
					m = "Missing zeropoint"
					return
				}
			}
		}
	case foreignKeyViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauge_measurements", "gauge_predictions", "bottlenecks":
				switch err.ConstraintName {
				case "gauge_key", "waterway_bottlenecks_reference_gauge",
					"waterway_gauge_measurements_reference_gauge":
					m = "Referenced gauge with matching temporal validity not available"
					return
				}
			}
		}
	case uniqueViolation:
		switch err.SchemaName {
		case "internal":
			switch err.TableName {
			case "user_profiles":
				switch err.ConstraintName {
				case "user_profiles_pkey":
					m = "A user with that name already exists"
					c = http.StatusConflict
					return
				}
			}
		}
	case checkViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "sounding_results":
				switch err.ConstraintName {
				case "b_sounding_results_in_bn_area":
					m = "Dataset does not intersect with given bottleneck"
					c = http.StatusConflict
					return
				}
			}
		}
	case noDataFound:
		// Most recent line from stacktrace contains name of failed function
		recent := strings.SplitN(err.Where, "\n", 1)[0]
		switch {
		case strings.Contains(recent, "isrsrange_points"):
			m = "No distance mark found for at least one given ISRS Location Code"
			return
		case strings.Contains(recent, "isrsrange_axis"):
			m = "No contiguous axis found between given ISRS Location Codes"
			return
		case strings.Contains(recent, "isrsrange_area"):
			m = "No area around axis between given ISRS Location Codes"
			return
		}
	case violatesRowLevelSecurity:
		m = "Could not save: Data outside the area of responsibility."
		return
	}
	m = "Unexpected database error: " + err.Message
	return
}