Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report error responses as JSON #90

Merged
merged 17 commits into from
Mar 5, 2025
Merged
82 changes: 2 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,87 +23,9 @@ Optional parameters:
It is possible to override the templates used to construct emails, and Github and Gitlab issues.
See [templates/README.md](templates/README.md) for more information.

## HTTP endpoints
## API Documentation

The following HTTP endpoints are exposed:

### GET `/api/listing/`

Serves submitted bug reports. Protected by basic HTTP auth using the
username/password provided in the environment. A browsable list, collated by
report submission date and time.

A whole directory can be downloaded as a tarball by appending the parameter `?format=tar.gz` to the end of the URL path

### POST `/api/submit`

Submission endpoint: this is where applications should send their reports.

The body of the request should be a multipart form-data submission, with the
following form field names. (For backwards compatibility, it can also be a JSON
object, but multipart is preferred as it allows more efficient transfer of the
logs.)

* `text`: A textual description of the problem. Included in the
`details.log.gz` file.

* `user_agent`: Application user-agent. Included in the `details.log.gz` file.

* `app`: Identifier for the application (eg 'riot-web'). Should correspond to a
mapping configured in the configuration file for github issue reporting to
work.

* `version`: Application version. Included in the `details.log.gz` file.

* `label`: Label to attach to the github issue, and include in the details file.

If using the JSON upload encoding, this should be encoded as a `labels` field,
whose value should be a list of strings.

* `log`: a log file, with lines separated by newline characters. Multiple log
files can be included by including several `log` parts.

If the log is uploaded with a filename `name.ext`, where `name` contains only
alphanumerics, `.`, `-` or `_`, and `ext` is one of `log` or `txt`, then the
file saved to disk is based on that. Otherwise, a suitable name is
constructed.

If using the JSON upload encoding, the request object should instead include
a single `logs` field, which is an array of objects with the following
fields:

* `id`: textual identifier for the logs. Used as the filename, as above.
* `lines`: log data. Newlines should be encoded as `\n`, as normal in JSON).

A summary of the current log file formats that are uploaded for `log` and
`compressed-log` is [available](docs/submitted_reports.md).

* `compressed-log`: a gzipped logfile. Decompressed and then treated the same as
`log`.

Compressed logs are not supported for the JSON upload encoding.

A summary of the current log file formats that are uploaded for `log` and
`compressed-log` is [available](docs/submitted_reports.md).

* `file`: an arbitrary file to attach to the report. Saved as-is to disk, and
a link is added to the github issue. The filename must be in the format
`name.ext`, where `name` contains only alphanumerics, `-` or `_`, and `ext`
is one of `jpg`, `png`, `txt`, `json`, `txt.gz` or `json.gz`.

Not supported for the JSON upload encoding.

* Any other form field names are interpreted as arbitrary name/value strings to
include in the `details.log.gz` file.

If using the JSON upload encoding, this additional metadata should insted be
encoded as a `data` field, whose value should be a JSON map. (Note that the
values must be strings; numbers, objects and arrays will be rejected.)

The response (if successful) will be a JSON object with the following fields:

* `report_url`: A URL where the user can track their bug report. Omitted if
issue submission was disabled.
See [docs/api.md](docs/api.md) for more information.

## Notifications

Expand Down
2 changes: 2 additions & 0 deletions changelog.d/99.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The `/api/submit` endpoint responds with JSON when it encounters an error.
Please read the documentation in [docs/api.md](https://github.com/matrix-org/rageshake/blob/main/docs/api.md) to learn more.
115 changes: 115 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
## HTTP endpoints

The following HTTP endpoints are exposed:

### GET `/api/listing/`

Serves submitted bug reports. Protected by basic HTTP auth using the
username/password provided in the environment. A browsable list, collated by
report submission date and time.

A whole directory can be downloaded as a tarball by appending the parameter `?format=tar.gz` to the end of the URL path

### POST `/api/submit`

Submission endpoint: this is where applications should send their reports.

The body of the request should be a multipart form-data submission, with the
following form field names. (For backwards compatibility, it can also be a JSON
object, but multipart is preferred as it allows more efficient transfer of the
logs.)

* `text`: A textual description of the problem. Included in the
`details.log.gz` file.

* `user_agent`: Application user-agent. Included in the `details.log.gz` file.

* `app`: Identifier for the application (eg 'riot-web'). Should correspond to a
mapping configured in the configuration file for github issue reporting to
work.

* `version`: Application version. Included in the `details.log.gz` file.

* `label`: Label to attach to the github issue, and include in the details file.

If using the JSON upload encoding, this should be encoded as a `labels` field,
whose value should be a list of strings.

* `log`: a log file, with lines separated by newline characters. Multiple log
files can be included by including several `log` parts.

If the log is uploaded with a filename `name.ext`, where `name` contains only
alphanumerics, `.`, `-` or `_`, and `ext` is one of `log` or `txt`, then the
file saved to disk is based on that. Otherwise, a suitable name is
constructed.

If using the JSON upload encoding, the request object should instead include
a single `logs` field, which is an array of objects with the following
fields:

* `id`: textual identifier for the logs. Used as the filename, as above.
* `lines`: log data. Newlines should be encoded as `\n`, as normal in JSON).

A summary of the current log file formats that are uploaded for `log` and
`compressed-log` is [available](docs/submitted_reports.md).

* `compressed-log`: a gzipped logfile. Decompressed and then treated the same as
`log`.

Compressed logs are not supported for the JSON upload encoding.

A summary of the current log file formats that are uploaded for `log` and
`compressed-log` is [available](docs/submitted_reports.md).

* `file`: an arbitrary file to attach to the report. Saved as-is to disk, and
a link is added to the github issue. The filename must be in the format
`name.ext`, where `name` contains only alphanumerics, `-` or `_`, and `ext`
is one of `jpg`, `png`, `txt`, `json`, `txt.gz` or `json.gz`.

Not supported for the JSON upload encoding.

* Any other form field names are interpreted as arbitrary name/value strings to
include in the `details.log.gz` file.

If using the JSON upload encoding, this additional metadata should insted be
encoded as a `data` field, whose value should be a JSON map. (Note that the
values must be strings; numbers, objects and arrays will be rejected.)

The response (if successful) will be a JSON object with the following fields:

* `report_url`: A URL where the user can track their bug report. Omitted if
issue submission was disabled.

## Error responses

The rageshake server will respond with a specific JSON payload when encountering an error.

```json
{
"error": "A human readable error string.",
"errcode": "UNKNOWN",
"policy_url": "https://github.com/matrix-org/rageshake/blob/master/docs/blocked_rageshake.md"
}
```

Where the fields are as follows:

- `error` is an error string to explain the error, in English.
- `errcode` is a machine readable error code which can be used by clients to give a localized error.
- `policy_url` is an optional URL that links to a reference document, which may be presented to users.

### Error codes

- `UNKNOWN` is a catch-all error when the appliation does not have a specific error.
- `METHOD_NOT_ALLOWED` is reported when you have used the wrong method for an endpoint. E.g. GET instead of POST.
- `DISALLOWED_APP` is reported when a report was rejected due to the report being sent from an unsupported
app (see the `allowed_app_names` config option).
- `BAD_HEADER` is reported when a header was not able to be parsed, such as `Content-Length`.
- `CONTENT_TOO_LARGE` is reported when the reported content size is too large.
- `BAD_CONTENT` is reported when the reports content could not be parsed.
- `REJECTED` is reported when the submission could be understood but was rejected by `rejection_conditions`.
This is the default value, see below for more information.

In addition to these error codes, the configuration allows application developers to specify specific error codes
for report rejection under the `REJECTED_*` namespace. (see the `rejection_conditions` config option). Consult the
administrator of your rageshake server in order to determine what error codes may be presented.
17 changes: 17 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main
const (
// ErrCodeBadContent is reported when the reports content could not be parsed.
ErrCodeBadContent = "BAD_CONTENT";
// ErrCodeBadHeader is reported when a header was not able to be parsed.
ErrCodeBadHeader = "BAD_HEADER";
// ErrCodeContentTooLarge is reported when the reported content size is too large.
ErrCodeContentTooLarge = "CONTENT_TOO_LARGE";
// ErrCodeDisallowedApp is reported when a report was rejected due to the report being sent from an unsupported
ErrCodeDisallowedApp = "DISALLOWED_APP";
// ErrCodeMethodNotAllowed is reported when you have used the wrong method for an endpoint.
ErrCodeMethodNotAllowed = "METHOD_NOT_ALLOWED";
// ErrCodeRejected is reported when the submission could be understood but was rejected by RejectionConditions.
ErrCodeRejected = "REJECTED";
// ErrCodeUnknown is a catch-all error when the appliation does not have a specific error.
ErrCodeUnknown = "UNKNOWN";
)
35 changes: 26 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ var DefaultEmailBodyTemplate string
var configPath = flag.String("config", "rageshake.yaml", "The path to the config file. For more information, see the config file in this repository.")
var bindAddr = flag.String("listen", ":9110", "The port to listen on.")

// defaultErrorReason is the default reason string when not present for a rejection condition
const defaultErrorReason string = "app or user text rejected"

type config struct {
// Username and password required to access the bug report listings
BugsUser string `yaml:"listings_auth_user"`
Expand Down Expand Up @@ -111,6 +114,8 @@ type RejectionCondition struct {
UserTextMatch string `yaml:"usertext"`
// Send this text to the client-side to inform the user why the server rejects the rageshake. Uses a default generic value if empty.
Reason string `yaml:"reason"`
// Send this text to the client-side to inform the user why the server rejects the rageshake. Uses a default error code REJECTED if empty.
ErrorCode string `yaml:"errorcode"`
}

func (c RejectionCondition) matchesApp(p *payload) bool {
Expand Down Expand Up @@ -148,26 +153,32 @@ func (c RejectionCondition) matchesUserText(p *payload) bool {
return c.UserTextMatch == "" || regexp.MustCompile(c.UserTextMatch).MatchString(p.UserText)
}

func (c RejectionCondition) shouldReject(p *payload) *string {
// Returns a rejection reason and error code if the payload should be rejected by this condition, condition; otherwise returns `nil` for both results.
func (c RejectionCondition) shouldReject(p *payload) (*string, *string) {
if c.matchesApp(p) && c.matchesVersion(p) && c.matchesLabel(p) && c.matchesUserText(p) {
// RejectionCondition matches all of the conditions: we should reject this submission/
defaultReason := "app or user text rejected"
var reason = defaultErrorReason
if c.Reason != "" {
return &c.Reason
reason = c.Reason
}
var code = ErrCodeRejected
if c.ErrorCode != "" {
code = c.ErrorCode
}
return &defaultReason
return &reason, &code
}
return nil
return nil, nil
}

func (c *config) matchesRejectionCondition(p *payload) *string {
// Returns a rejection reason and error code if the payload should be rejected by any condition, condition; otherwise returns `nil` for both results.
func (c *config) matchesRejectionCondition(p *payload) (*string, *string) {
for _, rc := range c.RejectionConditions {
reject := rc.shouldReject(p)
reject, code := rc.shouldReject(p)
if reject != nil {
return reject
return reject, code
}
}
return nil
return nil, nil
}

func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
Expand Down Expand Up @@ -341,5 +352,11 @@ func loadConfig(configPath string) (*config, error) {
if err = yaml.Unmarshal(contents, &cfg); err != nil {
return nil, err
}

for idx, condition := range cfg.RejectionConditions {
if condition.ErrorCode != "" && !strings.HasPrefix(condition.ErrorCode, "REJECTED_") {
return nil, fmt.Errorf("Rejected condition %d was invalid. `errorcode` must be use the namespace REJECTED_", idx);
}
}
return &cfg, nil
}
Loading
Loading