Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:
strategy:
matrix:
go-version: [1.22.x, 1.23.x, 1.24.x]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}

- name: Test
run: make test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ _testmain.go
*.prof

# Coverage reports
coverage.out
cover.out
15 changes: 0 additions & 15 deletions .travis.yml

This file was deleted.

61 changes: 44 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
PACKAGE=github.com/moogar0880/problems
###############################################################################
# GNU Make Variables
###############################################################################
export MAKEFLAGS += --warn-undefined-variables
export SHELL := bash
export .SHELLFLAGS := -eu -o pipefail -c
export .DEFAULT_GOAL := all
.DELETE_ON_ERROR:
.SUFFIXES:

.PHONY: all test coverage style
###############################################################################
# Location Variables
###############################################################################
export PROJECT_ROOT=$(shell pwd)

all: test
###############################################################################
# Go Variables
###############################################################################
GO_MODULE_NAME=github.com/moogar0880/problems
GO_COVERAGE_FILE=cover.out
GO_TEST_OPTS=-coverprofile $(GO_COVERAGE_FILE)
GO_TEST_PKGS=./...

build:
go build -v $(PACKAGE)
.PHONY: clean
clean:
@rm $(GO_COVERAGE_FILE)

### Testing
test: build
go test -v $(PACKAGE)

coverage:
go test -v -cover -coverprofile=coverage.out $(PACKAGE)
.PHONY: godoc
godoc:
docker run \
--rm \
--detach \
--entrypoint bash \
--expose "6060" \
--name "godoc" \
--publish "6060:6060" \
--volume $(PROJECT_ROOT):/go/src/$(GO_MODULE_NAME) \
--workdir /go/src/$(GO_MODULE_NAME) \
golang:latest \
-c "go install golang.org/x/tools/cmd/godoc@latest && godoc -http=:6060" || true
@open http://localhost:6060

### Style and Linting
lint:
go vet $(PACKAGE) && goimports .
### Testing
.PHONY: test
test:
@go test $(GO_TEST_OPTS) $(GO_TEST_PKGS)

# modify source code if style offences are found
style:
go vet $(PACKAGE) && goimports -e -l -w .
.PHONY: test/coverage
test/coverage: test
@go tool cover -html=cover.out
76 changes: 56 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# problems
Problems is an RFC-7807 compliant library for describing HTTP errors, written
purely in Go. For more information see [RFC-7807](https://tools.ietf.org/html/rfc7807).
Problems is an RFC-7807 and RFC-9457 compliant library for describing HTTP
errors. For more information see [RFC-9457](https://tools.ietf.org/html/rfc9457),
and it's predecessor [RFC-7807](https://tools.ietf.org/html/rfc7807).

[![Build Status](https://travis-ci.org/moogar0880/problems.svg?branch=master)](https://travis-ci.org/moogar0880/problems)
[![Go Report Card](https://goreportcard.com/badge/github.com/moogar0880/problems)](https://goreportcard.com/report/github.com/moogar0880/problems)
[![GoDoc](https://godoc.org/github.com/moogar0880/problems?status.svg)](https://godoc.org/github.com/moogar0880/problems)

## Usage
The problems library exposes an assortment of interfaces, structs, and functions
for defining and using HTTP Problem detail resources.
The problems library exposes an assortment of types to aid HTTP service authors
in defining and using HTTP Problem detail resources.

### Predefined Errors
You can define basic Problem details up front by using the `NewStatusProblem`
function

```go
package main

import "github.com/moogar0880/problems"

var (
Expand All @@ -39,6 +42,8 @@ Which, when served over HTTP as JSON will look like the following:
New errors can also be created a head of time, or on the fly like so:

```go
package main

import "github.com/moogar0880/problems"

func NoSuchUser() *problems.DefaultProblem {
Expand All @@ -59,31 +64,60 @@ Which, when served over HTTP as JSON will look like the following:
}
```

### Expanded Errors
### Extended Errors
The specification for these HTTP problems was designed to allow for arbitrary
expansion of the problem resources. This can be accomplished through this
library by implementing the `Problem` interface:
library by either embedding the `Problem` struct in your extension type:

```go
package main

import "github.com/moogar0880/problems"

type CreditProblem struct {
problems.DefaultProblem
problems.Problem

Balance float64 `json:"balance"`
Accounts []string `json:"accounts"`
}
```

Which, when served over HTTP as JSON will look like the following:

Balance float64
Accounts []string
```json
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
```

Or by using the `problems.ExtendedProblem` type:

```go
package main

import (
"net/http"

"github.com/moogar0880/problems"
)

func (cp *CreditProblem) ProblemType() (*url.URL, error) {
u, err := url.Parse(cp.Type)
if err != nil {
return nil, err
}
return u, nil
type CreditProblemExt struct {
Balance float64 `json:"balance"`
Accounts []string `json:"accounts"`
}

func (cp *CreditProblem) ProblemTitle() string {
return cp.Title
func main() {
problems.NewExt[CreditProblemExt]().
WithStatus(http.StatusForbidden).
WithDetail("Your account does not have sufficient funds to complete this transaction").
WithExtension(CreditProblemExt{
Balance: 30,
Accounts: []string{"/account/12345", "/account/67890"},
})
}
```

Expand All @@ -93,9 +127,11 @@ Which, when served over HTTP as JSON will look like the following:
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
"status": 401,
"extensions": {
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
}
```

Expand Down
26 changes: 17 additions & 9 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// Package problems provides an RFC 7807 (https://tools.ietf.org/html/rfc7807)
// compliant implementation of HTTP problem details. Which are defined as a
// means to carry machine-readable details of errors in an HTTP response to
// avoid the need to define new error response formats for HTTP APIs.
// Package problems provides an RFC-9457 (https://tools.ietf.org/html/rfc9457)
// and RFC 7807 (https://tools.ietf.org/html/rfc7807) compliant implementation
// of HTTP problem details. Which are defined as a means to carry
// machine-readable details of errors in an HTTP response to avoid the need to
// define new error response formats for HTTP APIs.
//
// The problem details specification was designed to allow for schema
// extensions. Because of this the exposed Problem interface only enforces the
// required Type and Title fields be set appropriately.
// extensions. There are two possible ways to create problem extensions:
//
// Additionally, this library also ships with default http.HandlerFunc's capable
// of writing problems to http.ResponseWriter's in either of the two standard
// media formats JSON and XML.
// 1. You can embed a problem in your extension problem type.
// 2. You can use the ExtendedProblem to leverage the existing types in this
// library.
//
// See the examples for references on how to use either of these extension
// mechanisms.
//
// Additionally, this library also ships with default http.HandlerFunc
// implementations which are capable of writing problems to a
// http.ResponseWriter in either of the two standard media formats, JSON and
// XML.
package problems
77 changes: 77 additions & 0 deletions doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package problems_test

import (
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/moogar0880/problems"
)
Expand Down Expand Up @@ -30,3 +32,78 @@ func ExampleNewStatusProblem_detailed() {
// "detail": "The item you've requested either does not exist or has been deleted."
// }
}

func ExampleFromError() {
err := func() error {
// Some fallible function.
return errors.New("something bad happened")
}()
internalServerError := problems.FromError(err).WithStatus(http.StatusInternalServerError)
b, _ := json.MarshalIndent(internalServerError, "", " ")
fmt.Println(string(b))
// Output: {
// "type": "about:blank",
// "title": "Internal Server Error",
// "status": 500,
// "detail": "something bad happened"
// }
}

func ExampleExtendedProblem() {
type CreditProblemExt struct {
Balance float64 `json:"balance"`
Accounts []string `json:"accounts"`
}
problem := problems.NewExt[CreditProblemExt]().
WithStatus(http.StatusForbidden).
WithDetail("You do not have sufficient funds to complete this transaction.").
WithExtension(CreditProblemExt{
Balance: 30,
Accounts: []string{"/account/12345", "/account/67890"},
})
b, _ := json.MarshalIndent(problem, "", " ")
fmt.Println(string(b))
// Output: {
// "type": "about:blank",
// "title": "Forbidden",
// "status": 403,
// "detail": "You do not have sufficient funds to complete this transaction.",
// "extensions": {
// "balance": 30,
// "accounts": [
// "/account/12345",
// "/account/67890"
// ]
// }
// }
}

func ExampleExtendedProblem_embedding() {
type CreditProblem struct {
problems.Problem

Balance float64 `json:"balance"`
Accounts []string `json:"accounts"`
}
problem := &CreditProblem{
Problem: *problems.New().
WithStatus(http.StatusForbidden).
WithDetail("You do not have sufficient funds to complete this transaction."),
Balance: 30,
Accounts: []string{"/account/12345", "/account/67890"},
}

b, _ := json.MarshalIndent(problem, "", " ")
fmt.Println(string(b))
// Output: {
// "type": "about:blank",
// "title": "Forbidden",
// "status": 403,
// "detail": "You do not have sufficient funds to complete this transaction.",
// "balance": 30,
// "accounts": [
// "/account/12345",
// "/account/67890"
// ]
// }
}
8 changes: 5 additions & 3 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ var ErrTitleMustBeSet = fmt.Errorf("%s: problem title must be set", errPrefix)
// valid URI when it is validated. The inner Err will contain the error
// returned from attempting to parse the invalid URI.
type ErrInvalidProblemType struct {
Err error
Err error
Value string
}

// NewErrInvalidProblemType returns a new ErrInvalidProblemType instance which
// wraps the provided error.
func NewErrInvalidProblemType(e error) error {
func NewErrInvalidProblemType(value string, e error) error {
return &ErrInvalidProblemType{
Err: e,
Err: e,
Value: value,
}
}

Expand Down
Loading