mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
fix: add information about target response into error message if inte… (#8281)
# Which Problems Are Solved Execution responses with HTTP StatusCode not equal to 200 interrupt the client request silently. # How the Problems Are Solved Adds information about the recieved StatusCode and Body into the error if StatusCode not 200. # Additional Context Closes #8177 --------- Co-authored-by: Elio Bischof <elio@zitadel.com> Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
11d01b9b35
commit
83c78a470c
@ -169,4 +169,19 @@ For event there are 3 levels the condition can be defined:
|
||||
- Group, handling a specific group of events
|
||||
- All, handling any event in ZITADEL
|
||||
|
||||
The concept of events can be found under [Events](/concepts/architecture/software#events)
|
||||
The concept of events can be found under [Events](/concepts/architecture/software#events)
|
||||
|
||||
### Error forwarding
|
||||
|
||||
If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"forwardedStatusCode": 403,
|
||||
"forwardedErrorMessage": "Call is forbidden through the IP AllowList definition"
|
||||
}
|
||||
```
|
||||
|
||||
Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error.
|
||||
|
||||
If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged.
|
||||
|
@ -263,7 +263,7 @@ func TestServer_RemovePhone(t *testing.T) {
|
||||
req *user.RemovePhoneRequest
|
||||
want *user.RemovePhoneResponse
|
||||
wantErr bool
|
||||
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
|
||||
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "remove phone",
|
||||
@ -303,7 +303,7 @@ func TestServer_RemovePhone(t *testing.T) {
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return Client.RemovePhone(ctx, &user.RemovePhoneRequest{
|
||||
UserId: doubleRemoveUser.GetUserId(),
|
||||
});
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -45,3 +45,36 @@ func ZitadelErrorToHTTPStatusCode(err error) (statusCode int, ok bool) {
|
||||
return http.StatusInternalServerError, false
|
||||
}
|
||||
}
|
||||
|
||||
func HTTPStatusCodeToZitadelError(parent error, statusCode int, id string, message string) error {
|
||||
if statusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
var errorFunc func(parent error, id, message string) error
|
||||
switch statusCode {
|
||||
case http.StatusConflict:
|
||||
errorFunc = zerrors.ThrowAlreadyExists
|
||||
case http.StatusGatewayTimeout:
|
||||
errorFunc = zerrors.ThrowDeadlineExceeded
|
||||
case http.StatusInternalServerError:
|
||||
errorFunc = zerrors.ThrowInternal
|
||||
case http.StatusBadRequest:
|
||||
errorFunc = zerrors.ThrowInvalidArgument
|
||||
case http.StatusNotFound:
|
||||
errorFunc = zerrors.ThrowNotFound
|
||||
case http.StatusForbidden:
|
||||
errorFunc = zerrors.ThrowPermissionDenied
|
||||
case http.StatusUnauthorized:
|
||||
errorFunc = zerrors.ThrowUnauthenticated
|
||||
case http.StatusServiceUnavailable:
|
||||
errorFunc = zerrors.ThrowUnavailable
|
||||
case http.StatusNotImplemented:
|
||||
errorFunc = zerrors.ThrowUnimplemented
|
||||
case http.StatusTooManyRequests:
|
||||
errorFunc = zerrors.ThrowResourceExhausted
|
||||
default:
|
||||
errorFunc = zerrors.ThrowUnknown
|
||||
}
|
||||
|
||||
return errorFunc(parent, id, message)
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@ -136,3 +138,152 @@ func TestZitadelErrorToHTTPStatusCode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPStatusCodeToZitadelError(t *testing.T) {
|
||||
type args struct {
|
||||
statusCode int
|
||||
id string
|
||||
message string
|
||||
parent error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "StatusOK",
|
||||
args: args{
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "StatusConflict",
|
||||
args: args{
|
||||
statusCode: http.StatusConflict,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowAlreadyExists(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusGatewayTimeout",
|
||||
args: args{
|
||||
statusCode: http.StatusGatewayTimeout,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowDeadlineExceeded(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusInternalServerError",
|
||||
args: args{
|
||||
statusCode: http.StatusInternalServerError,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowInternal(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusBadRequest",
|
||||
args: args{
|
||||
statusCode: http.StatusBadRequest,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusNotFound",
|
||||
args: args{
|
||||
statusCode: http.StatusNotFound,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusForbidden",
|
||||
args: args{
|
||||
statusCode: http.StatusForbidden,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusUnauthorized",
|
||||
args: args{
|
||||
statusCode: http.StatusUnauthorized,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowUnauthenticated(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusServiceUnavailable",
|
||||
args: args{
|
||||
statusCode: http.StatusServiceUnavailable,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowUnavailable(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusNotImplemented",
|
||||
args: args{
|
||||
statusCode: http.StatusNotImplemented,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowUnimplemented(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "StatusTooManyRequests",
|
||||
args: args{
|
||||
statusCode: http.StatusTooManyRequests,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowResourceExhausted(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
args: args{
|
||||
statusCode: 1000,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowUnknown(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "Unknown, test for statuscode",
|
||||
args: args{
|
||||
statusCode: 1000,
|
||||
id: "id",
|
||||
message: "message",
|
||||
},
|
||||
wantErr: zerrors.ThrowError(nil, "id", "message"),
|
||||
},
|
||||
{
|
||||
name: "Unknown with parent",
|
||||
args: args{
|
||||
statusCode: 1000,
|
||||
id: "id",
|
||||
message: "message",
|
||||
parent: errors.New("parent error"),
|
||||
},
|
||||
wantErr: zerrors.ThrowUnknown(nil, "id", "message"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := HTTPStatusCodeToZitadelError(tt.args.parent, tt.args.statusCode, tt.args.id, tt.args.message)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.args.parent != nil {
|
||||
assert.ErrorIs(t, err, tt.args.parent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,14 @@ package execution
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
zhttp "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -114,9 +116,35 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return HandleResponse(resp)
|
||||
}
|
||||
|
||||
func HandleResponse(resp *http.Response) ([]byte, error) {
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check for success between 200 and 299, redirect 300 to 399 is handled by the client, return error with statusCode >= 400
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
|
||||
return io.ReadAll(resp.Body)
|
||||
var errorBody ErrorBody
|
||||
if err := json.Unmarshal(data, &errorBody); err != nil {
|
||||
// if json unmarshal fails, body has no ErrorBody information, so will be taken as successful response
|
||||
return data, nil
|
||||
}
|
||||
if errorBody.ForwardedStatusCode != 0 || errorBody.ForwardedErrorMessage != "" {
|
||||
if errorBody.ForwardedStatusCode >= 400 && errorBody.ForwardedStatusCode < 500 {
|
||||
return nil, zhttp.HTTPStatusCodeToZitadelError(nil, errorBody.ForwardedStatusCode, "EXEC-reUaUZCzCp", errorBody.ForwardedErrorMessage)
|
||||
}
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "EXEC-bmhNhpcqpF", errorBody.ForwardedErrorMessage)
|
||||
}
|
||||
// no ErrorBody filled in response, so will be taken as successful response
|
||||
return data, nil
|
||||
}
|
||||
return nil, zerrors.ThrowUnknown(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")
|
||||
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")
|
||||
}
|
||||
|
||||
type ErrorBody struct {
|
||||
ForwardedStatusCode int `json:"forwardedStatusCode,omitempty"`
|
||||
ForwardedErrorMessage string `json:"forwardedErrorMessage,omitempty"`
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package execution_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -15,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func Test_Call(t *testing.T) {
|
||||
@ -513,3 +517,152 @@ var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}")
|
||||
type request struct {
|
||||
Request string `json:"request"`
|
||||
}
|
||||
|
||||
func testErrorBody(code int, message string) []byte {
|
||||
body := &execution.ErrorBody{ForwardedStatusCode: code, ForwardedErrorMessage: message}
|
||||
data, _ := json.Marshal(body)
|
||||
return data
|
||||
}
|
||||
|
||||
func Test_handleResponse(t *testing.T) {
|
||||
type args struct {
|
||||
resp *http.Response
|
||||
}
|
||||
type res struct {
|
||||
data []byte
|
||||
wantErr func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"response, statuscode unknown and body",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: 1000,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode >= 400 and no body",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode >= 400 and body",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("body"))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed"))
|
||||
}},
|
||||
},
|
||||
{
|
||||
"response, statuscode = 200 and body",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("body"))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
data: []byte("body"),
|
||||
wantErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode = 200 no body",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
data: []byte(""),
|
||||
wantErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode = 200, error body >= 400 < 500",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(testErrorBody(http.StatusForbidden, "forbidden"))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "EXEC-reUaUZCzCp", "forbidden"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode = 200, error body >= 500",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(testErrorBody(http.StatusInternalServerError, "internal"))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-bmhNhpcqpF", "internal"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"response, statuscode = 308, no body, should not happen",
|
||||
args{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(testErrorBody(http.StatusPermanentRedirect, "redirect"))),
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-bmhNhpcqpF", "redirect"))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := execution.HandleResponse(
|
||||
tt.args.resp,
|
||||
)
|
||||
|
||||
if tt.res.wantErr == nil {
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
} else if !tt.res.wantErr(err) {
|
||||
t.Errorf("got wrong err: %v", err)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.res.data, respBody)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -581,6 +581,7 @@ Errors:
|
||||
NotFound: Изпълнението не е намерено
|
||||
IncludeNotFound: Включването не е намерено
|
||||
NoTargets: Няма определени цели
|
||||
Failed: неуспешно изпълнение
|
||||
ResponseIsNotValidJSON: Отговорът не е валиден JSON
|
||||
UserSchema:
|
||||
NotEnabled: Функцията „Потребителска схема“ не е активирана
|
||||
|
@ -562,6 +562,7 @@ Errors:
|
||||
NotFound: Provedení nenalezeno
|
||||
IncludeNotFound: Zahrnout nenalezeno
|
||||
NoTargets: Nejsou definovány žádné cíle
|
||||
Failed: Provedení se nezdařilo
|
||||
ResponseIsNotValidJSON: Odpověď není platný JSON
|
||||
UserSchema:
|
||||
NotEnabled: Funkce "Uživatelské schéma" není povolena
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Ausführung nicht gefunden
|
||||
IncludeNotFound: Einschließen nicht gefunden
|
||||
NoTargets: Keine Ziele definiert
|
||||
Failed: Ausführung fehlgeschlagen
|
||||
ResponseIsNotValidJSON: Antwort ist kein gültiges JSON
|
||||
UserSchema:
|
||||
NotEnabled: Funktion Benutzerschema ist nicht aktiviert
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Execution not found
|
||||
IncludeNotFound: Include not found
|
||||
NoTargets: No targets defined
|
||||
Failed: Execution failed
|
||||
ResponseIsNotValidJSON: Response is not valid JSON
|
||||
UserSchema:
|
||||
NotEnabled: Feature "User Schema" is not enabled
|
||||
@ -594,7 +595,6 @@ Errors:
|
||||
NoActive: No active web key found
|
||||
NotFound: Web key not found
|
||||
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
instance: Instance
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Ejecución no encontrada
|
||||
IncludeNotFound: Incluir no encontrado
|
||||
NoTargets: No hay objetivos definidos
|
||||
Failed: Ejecución fallida
|
||||
ResponseIsNotValidJSON: La respuesta no es un JSON válido
|
||||
UserSchema:
|
||||
NotEnabled: La función "Esquema de usuario" no está habilitada
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Exécution introuvable
|
||||
IncludeNotFound: Inclure introuvable
|
||||
NoTargets: Aucune cible définie
|
||||
Failed: Exécution échouée
|
||||
ResponseIsNotValidJSON: La réponse n'est pas un JSON valide
|
||||
UserSchema:
|
||||
NotEnabled: La fonctionnalité "Schéma utilisateur" n'est pas activée
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Esecuzione non trovata
|
||||
IncludeNotFound: Includi non trovato
|
||||
NoTargets: Nessun obiettivo definito
|
||||
Failed: Esecuzione fallita
|
||||
ResponseIsNotValidJSON: La risposta non è un JSON valido
|
||||
UserSchema:
|
||||
NotEnabled: La funzionalità "Schema utente" non è abilitata
|
||||
|
@ -553,6 +553,7 @@ Errors:
|
||||
NotFound: 実行が見つかりませんでした
|
||||
IncludeNotFound: 見つからないものを含める
|
||||
NoTargets: ターゲットが定義されていません
|
||||
Failed: 実行に失敗しました
|
||||
ResponseIsNotValidJSON: 応答は有効な JSON ではありません
|
||||
UserSchema:
|
||||
NotEnabled: 機能「ユーザースキーマ」が有効になっていません
|
||||
|
@ -563,6 +563,7 @@ Errors:
|
||||
NotFound: Извршувањето не е пронајдено
|
||||
IncludeNotFound: Вклучете не е пронајден
|
||||
NoTargets: Не се дефинирани цели
|
||||
Failed: Извршувањето не успеа
|
||||
ResponseIsNotValidJSON: Одговорот не е валиден JSON
|
||||
UserSchema:
|
||||
NotEnabled: Функцијата „Корисничка шема“ не е овозможена
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Uitvoering niet gevonden
|
||||
IncludeNotFound: Inclusief niet gevonden
|
||||
NoTargets: Geen doelstellingen gedefinieerd
|
||||
Failed: Uitvoering mislukt
|
||||
ResponseIsNotValidJSON: Reactie is geen geldige JSON
|
||||
UserSchema:
|
||||
NotEnabled: Functie "Gebruikersschema" is niet ingeschakeld
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Nie znaleziono wykonania
|
||||
IncludeNotFound: Nie znaleziono uwzględnienia
|
||||
NoTargets: Nie zdefiniowano celów
|
||||
Failed: Wykonanie nie powiodło się
|
||||
ResponseIsNotValidJSON: Odpowiedź nie jest prawidłowym JSON-em
|
||||
UserSchema:
|
||||
NotEnabled: Funkcja „Schemat użytkownika” nie jest włączona
|
||||
|
@ -559,6 +559,7 @@ Errors:
|
||||
NotFound: Execução não encontrada
|
||||
IncludeNotFound: Incluir não encontrado
|
||||
NoTargets: Nenhuma meta definida
|
||||
Failed: Falha na execução
|
||||
ResponseIsNotValidJSON: A resposta não é um JSON válido
|
||||
UserSchema:
|
||||
NotEnabled: O recurso "Esquema do usuário" não está habilitado
|
||||
|
@ -553,6 +553,7 @@ Errors:
|
||||
NotFound: Исполнение не найдено
|
||||
IncludeNotFound: Включить не найдено
|
||||
NoTargets: Цели не определены
|
||||
Failed: Выполнение не удалось
|
||||
ResponseIsNotValidJSON: Ответ не является допустимым JSON
|
||||
UserSchema:
|
||||
NotEnabled: Функция «Пользовательская схема» не включена
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: Exekveringen hittades inte
|
||||
IncludeNotFound: Inkluderingen hittades inte
|
||||
NoTargets: Inga mål definierade
|
||||
Failed: Utförande misslyckades
|
||||
ResponseIsNotValidJSON: Svaret är inte giltigt JSON
|
||||
UserSchema:
|
||||
NotEnabled: Funktionen "Användarschema" är inte aktiverad
|
||||
|
@ -564,6 +564,7 @@ Errors:
|
||||
NotFound: 未找到执行
|
||||
IncludeNotFound: 包括未找到的内容
|
||||
NoTargets: 没有定义目标
|
||||
Failed: 执行失败
|
||||
ResponseIsNotValidJSON: 响应不是有效的 JSON
|
||||
UserSchema:
|
||||
NotEnabled: 未启用“用户架构”功能
|
||||
|
Loading…
Reference in New Issue
Block a user