mirror of
https://github.com/go-kratos/kratos.git
synced 2025-03-17 21:07:54 +02:00
parent
5d9b5818f2
commit
9806191b7f
317
errors/codes.go
317
errors/codes.go
@ -1,317 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Cancelled The operation was cancelled, typically by the caller.
|
||||
// HTTP Mapping: 499 Client Closed Request
|
||||
func Cancelled(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 1,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsCancelled determines if err is an error which indicates a cancelled error.
|
||||
// It supports wrapped errors.
|
||||
func IsCancelled(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Unknown error.
|
||||
// HTTP Mapping: 500 Internal Server Error
|
||||
func Unknown(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 2,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnknown determines if err is an error which indicates a unknown error.
|
||||
// It supports wrapped errors.
|
||||
func IsUnknown(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 2
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InvalidArgument The client specified an invalid argument.
|
||||
// HTTP Mapping: 400 Bad Request
|
||||
func InvalidArgument(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 3,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsInvalidArgument determines if err is an error which indicates an invalid argument error.
|
||||
// It supports wrapped errors.
|
||||
func IsInvalidArgument(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 3
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DeadlineExceeded The deadline expired before the operation could complete.
|
||||
// HTTP Mapping: 504 Gateway Timeout
|
||||
func DeadlineExceeded(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 4,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsDeadlineExceeded determines if err is an error which indicates a deadline exceeded error.
|
||||
// It supports wrapped errors.
|
||||
func IsDeadlineExceeded(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 4
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NotFound Some requested entity (e.g., file or directory) was not found.
|
||||
// HTTP Mapping: 404 Not Found
|
||||
func NotFound(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 5,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotFound determines if err is an error which indicates a not found error.
|
||||
// It supports wrapped errors.
|
||||
func IsNotFound(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 5
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AlreadyExists The entity that a client attempted to create (e.g., file or directory) already exists.
|
||||
// HTTP Mapping: 409 Conflict
|
||||
func AlreadyExists(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 6,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsAlreadyExists determines if err is an error which indicates a already exsits error.
|
||||
// It supports wrapped errors.
|
||||
func IsAlreadyExists(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 6
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PermissionDenied The caller does not have permission to execute the specified operation.
|
||||
// HTTP Mapping: 403 Forbidden
|
||||
func PermissionDenied(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 7,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsPermissionDenied determines if err is an error which indicates a permission denied error.
|
||||
// It supports wrapped errors.
|
||||
func IsPermissionDenied(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 7
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResourceExhausted Some resource has been exhausted, perhaps a per-user quota, or
|
||||
// perhaps the entire file system is out of space.
|
||||
// HTTP Mapping: 429 Too Many Requests
|
||||
func ResourceExhausted(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 8,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsResourceExhausted determines if err is an error which indicates a resource exhausted error.
|
||||
// It supports wrapped errors.
|
||||
func IsResourceExhausted(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 8
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FailedPrecondition The operation was rejected because the system is not in a state
|
||||
// required for the operation's execution.
|
||||
// HTTP Mapping: 400 Bad Request
|
||||
func FailedPrecondition(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 9,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsFailedPrecondition determines if err is an error which indicates a failed precondition error.
|
||||
// It supports wrapped errors.
|
||||
func IsFailedPrecondition(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 9
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Aborted The operation was aborted, typically due to a concurrency issue such as
|
||||
// a sequencer check failure or transaction abort.
|
||||
// HTTP Mapping: 409 Conflict
|
||||
func Aborted(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 10,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsAborted determines if err is an error which indicates an aborted error.
|
||||
// It supports wrapped errors.
|
||||
func IsAborted(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 10
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OutOfRange The operation was attempted past the valid range. E.g., seeking or
|
||||
// reading past end-of-file.
|
||||
// HTTP Mapping: 400 Bad Request
|
||||
func OutOfRange(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 11,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsOutOfRange determines if err is an error which indicates a out of range error.
|
||||
// It supports wrapped errors.
|
||||
func IsOutOfRange(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 11
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Unimplemented The operation is not implemented or is not supported/enabled in this service.
|
||||
// HTTP Mapping: 501 Not Implemented
|
||||
func Unimplemented(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 12,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnimplemented determines if err is an error which indicates a unimplemented error.
|
||||
// It supports wrapped errors.
|
||||
func IsUnimplemented(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 12
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Internal This means that some invariants expected by the
|
||||
// underlying system have been broken. This error code is reserved
|
||||
// for serious errors.
|
||||
//
|
||||
// HTTP Mapping: 500 Internal Server Error
|
||||
func Internal(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 13,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsInternal determines if err is an error which indicates an internal server error.
|
||||
// It supports wrapped errors.
|
||||
func IsInternal(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 13
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Unavailable The service is currently unavailable.
|
||||
// HTTP Mapping: 503 Service Unavailable
|
||||
func Unavailable(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 14,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnavailable determines if err is an error which indicates a unavailable error.
|
||||
// It supports wrapped errors.
|
||||
func IsUnavailable(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 14
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DataLoss Unrecoverable data loss or corruption.
|
||||
// HTTP Mapping: 500 Internal Server Error
|
||||
func DataLoss(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 15,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsDataLoss determines if err is an error which indicates a data loss error.
|
||||
// It supports wrapped errors.
|
||||
func IsDataLoss(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 15
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Unauthorized The request does not have valid authentication credentials for the operation.
|
||||
// HTTP Mapping: 401 Unauthorized
|
||||
func Unauthorized(reason, format string, a ...interface{}) error {
|
||||
return &StatusError{
|
||||
Code: 16,
|
||||
Reason: reason,
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnauthorized determines if err is an error which indicates a unauthorized error.
|
||||
// It supports wrapped errors.
|
||||
func IsUnauthorized(err error) bool {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code == 16
|
||||
}
|
||||
return false
|
||||
}
|
125
errors/errors.go
125
errors/errors.go
@ -7,110 +7,83 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// UnknownReason is unknown reason for error info.
|
||||
UnknownReason = ""
|
||||
// SupportPackageIsVersion1 this constant should not be referenced by any other code.
|
||||
SupportPackageIsVersion1 = true
|
||||
)
|
||||
|
||||
var _ error = (*StatusError)(nil)
|
||||
// Error contains an error response from the server.
|
||||
// For more details see https://github.com/go-kratos/kratos/issues/858.
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Domain string `json:"domain"`
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// StatusError contains an error response from the server.
|
||||
type StatusError = Status
|
||||
// WithMetadata with an MD formed by the mapping of key, value.
|
||||
func (e *Error) WithMetadata(md map[string]string) *Error {
|
||||
err := *e
|
||||
err.Metadata = md
|
||||
return &err
|
||||
}
|
||||
|
||||
// Is matches each error in the chain with the target value.
|
||||
func (e *StatusError) Is(target error) bool {
|
||||
err, ok := target.(*StatusError)
|
||||
if ok {
|
||||
return e.Code == err.Code
|
||||
func (e *Error) Is(err error) bool {
|
||||
if target := new(Error); errors.As(err, &target) {
|
||||
return target.Code == e.Code && target.Reason == e.Reason
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HTTPStatus returns the Status represented by se.
|
||||
func (e *StatusError) HTTPStatus() int {
|
||||
switch e.Code {
|
||||
case 0:
|
||||
return http.StatusOK
|
||||
case 1:
|
||||
return http.StatusInternalServerError
|
||||
case 2:
|
||||
return http.StatusInternalServerError
|
||||
case 3:
|
||||
return http.StatusBadRequest
|
||||
case 4:
|
||||
return http.StatusRequestTimeout
|
||||
case 5:
|
||||
return http.StatusNotFound
|
||||
case 6:
|
||||
return http.StatusConflict
|
||||
case 7:
|
||||
return http.StatusForbidden
|
||||
case 8:
|
||||
return http.StatusTooManyRequests
|
||||
case 9:
|
||||
return http.StatusPreconditionFailed
|
||||
case 10:
|
||||
return http.StatusConflict
|
||||
case 11:
|
||||
return http.StatusBadRequest
|
||||
case 12:
|
||||
return http.StatusNotImplemented
|
||||
case 13:
|
||||
return http.StatusInternalServerError
|
||||
case 14:
|
||||
return http.StatusServiceUnavailable
|
||||
case 15:
|
||||
return http.StatusInternalServerError
|
||||
case 16:
|
||||
return http.StatusUnauthorized
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("error: code = %d domain = %s reason = %s message = %s metadata = %v", e.Code, e.Domain, e.Reason, e.Message, e.Metadata)
|
||||
}
|
||||
|
||||
func (e *StatusError) Error() string {
|
||||
return fmt.Sprintf("error: code = %d reason = %s message = %s details = %+v", e.Code, e.Reason, e.Message, e.Details)
|
||||
}
|
||||
|
||||
// Error returns a Status representing c and msg.
|
||||
func Error(code int32, reason, message string) error {
|
||||
return &StatusError{
|
||||
// New returns an error object for the code, message and error info.
|
||||
func New(code int, reason, message string) *Error {
|
||||
return &Error{
|
||||
Code: code,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf returns New(c, fmt.Sprintf(format, a...)).
|
||||
func Errorf(code int32, reason, format string, a ...interface{}) error {
|
||||
return Error(code, reason, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Code returns the status code.
|
||||
func Code(err error) int32 {
|
||||
// Code returns the code for a particular error.
|
||||
// It supports wrapped errors.
|
||||
func Code(err error) int {
|
||||
if err == nil {
|
||||
return 0 // ok
|
||||
return http.StatusOK
|
||||
}
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Code
|
||||
if target := new(Error); errors.As(err, &target) {
|
||||
return target.Code
|
||||
}
|
||||
return 2 // unknown
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// Reason returns the status for a particular error.
|
||||
// Domain returns the domain for a particular error.
|
||||
// It supports wrapped errors.
|
||||
func Domain(err error) string {
|
||||
if target := new(Error); errors.As(err, &target) {
|
||||
return target.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reason returns the reason for a particular error.
|
||||
// It supports wrapped errors.
|
||||
func Reason(err error) string {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se.Reason
|
||||
if target := new(Error); errors.As(err, &target) {
|
||||
return target.Reason
|
||||
}
|
||||
return UnknownReason
|
||||
return ""
|
||||
}
|
||||
|
||||
// FromError returns status error.
|
||||
func FromError(err error) (*StatusError, bool) {
|
||||
if se := new(StatusError); errors.As(err, &se) {
|
||||
return se, true
|
||||
// FromError try to convert an error to *Error.
|
||||
// It supports wrapped errors.
|
||||
func FromError(err error) *Error {
|
||||
if target := new(Error); errors.As(err, &target) {
|
||||
return target
|
||||
}
|
||||
return nil, false
|
||||
return New(http.StatusInternalServerError, "", err.Error())
|
||||
}
|
||||
|
@ -1,187 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.13.0
|
||||
// source: errors.proto
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
any "github.com/golang/protobuf/ptypes/any"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type Status struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
|
||||
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Details []*any.Any `protobuf:"bytes,4,rep,name=details,proto3" json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Status) Reset() {
|
||||
*x = Status{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_errors_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Status) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Status) ProtoMessage() {}
|
||||
|
||||
func (x *Status) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_errors_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Status.ProtoReflect.Descriptor instead.
|
||||
func (*Status) Descriptor() ([]byte, []int) {
|
||||
return file_errors_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Status) GetCode() int32 {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Status) GetReason() string {
|
||||
if x != nil {
|
||||
return x.Reason
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Status) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Status) GetDetails() []*any.Any {
|
||||
if x != nil {
|
||||
return x.Details
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_errors_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_errors_proto_rawDesc = []byte{
|
||||
0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d,
|
||||
0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x19, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61,
|
||||
0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
|
||||
0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18,
|
||||
0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61,
|
||||
0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52,
|
||||
0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x62, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x2e,
|
||||
0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x42, 0x0b, 0x45,
|
||||
0x72, 0x72, 0x6f, 0x72, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74,
|
||||
0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x65, 0x72, 0x72,
|
||||
0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0xf8, 0x01, 0x01, 0xa2, 0x02, 0x0c,
|
||||
0x4b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_errors_proto_rawDescOnce sync.Once
|
||||
file_errors_proto_rawDescData = file_errors_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_errors_proto_rawDescGZIP() []byte {
|
||||
file_errors_proto_rawDescOnce.Do(func() {
|
||||
file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_proto_rawDescData)
|
||||
})
|
||||
return file_errors_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_errors_proto_goTypes = []interface{}{
|
||||
(*Status)(nil), // 0: kratos.errors.Status
|
||||
(*any.Any)(nil), // 1: google.protobuf.Any
|
||||
}
|
||||
var file_errors_proto_depIdxs = []int32{
|
||||
1, // 0: kratos.errors.Status.details:type_name -> google.protobuf.Any
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_errors_proto_init() }
|
||||
func file_errors_proto_init() {
|
||||
if File_errors_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Status); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_errors_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_errors_proto_goTypes,
|
||||
DependencyIndexes: file_errors_proto_depIdxs,
|
||||
MessageInfos: file_errors_proto_msgTypes,
|
||||
}.Build()
|
||||
File_errors_proto = out.File
|
||||
file_errors_proto_rawDesc = nil
|
||||
file_errors_proto_goTypes = nil
|
||||
file_errors_proto_depIdxs = nil
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package kratos.errors;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "github.com/go-kratos/kratos/v2/errors;errors";
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "ErrorsProto";
|
||||
option java_package = "com.github.kratos.errors";
|
||||
option objc_class_prefix = "KratosErrors";
|
||||
|
||||
message Status {
|
||||
int32 code = 1;
|
||||
string reason = 2;
|
||||
string message = 3;
|
||||
repeated google.protobuf.Any details = 4;
|
||||
}
|
@ -6,49 +6,45 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErrorsMatch(t *testing.T) {
|
||||
s := &StatusError{Code: 1}
|
||||
st := &StatusError{Code: 2}
|
||||
func TestError(t *testing.T) {
|
||||
var (
|
||||
base *Error
|
||||
)
|
||||
err := New(400, "reason", "message")
|
||||
err2 := New(400, "reason", "message")
|
||||
err3 := err.WithMetadata(map[string]string{
|
||||
"foo": "bar",
|
||||
})
|
||||
werr := fmt.Errorf("wrap %w", err)
|
||||
|
||||
if errors.Is(s, st) {
|
||||
t.Errorf("error is not match: %+v -> %+v", s, st)
|
||||
if errors.Is(err, new(Error)) {
|
||||
t.Errorf("should not be equal: %v", err)
|
||||
}
|
||||
if !errors.Is(werr, err) {
|
||||
t.Errorf("should be equal: %v", err)
|
||||
}
|
||||
if !errors.Is(werr, err2) {
|
||||
t.Errorf("should be equal: %v", err)
|
||||
}
|
||||
|
||||
s.Code = 1
|
||||
st.Code = 1
|
||||
if !errors.Is(s, st) {
|
||||
t.Errorf("error is not match: %+v -> %+v", s, st)
|
||||
if !errors.As(err, &base) {
|
||||
t.Errorf("should be matchs: %v", err)
|
||||
}
|
||||
if !IsBadRequest(err) {
|
||||
t.Errorf("should be matchs: %v", err)
|
||||
}
|
||||
|
||||
s.Reason = "test_reason"
|
||||
s.Reason = "test_reason"
|
||||
|
||||
if !errors.Is(s, st) {
|
||||
t.Errorf("error is not match: %+v -> %+v", s, st)
|
||||
if code := Code(err); code != err2.Code {
|
||||
t.Errorf("got %d want: %s", code, err)
|
||||
}
|
||||
if domain := Domain(err); domain != err2.Domain {
|
||||
t.Errorf("got %s want: %s", domain, err)
|
||||
}
|
||||
if reason := Reason(err); reason != err3.Reason {
|
||||
t.Errorf("got %s want: %s", reason, err)
|
||||
}
|
||||
|
||||
if Reason(s) != "test_reason" {
|
||||
t.Errorf("error is not match: %+v -> %+v", s, st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
err1 := &StatusError{Code: 1}
|
||||
t.Log(err1)
|
||||
err2 := fmt.Errorf("wrap : %w", err1)
|
||||
t.Log(err2)
|
||||
|
||||
if !(errors.Is(err2, err1)) {
|
||||
t.Errorf("error is not match: a: %v b: %v ", err2, err1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAs(t *testing.T) {
|
||||
err1 := &StatusError{Code: 1}
|
||||
err2 := fmt.Errorf("wrap : %w", err1)
|
||||
|
||||
err3 := new(StatusError)
|
||||
if !errors.As(err2, &err3) {
|
||||
t.Errorf("error is not match: %v", err2)
|
||||
if err3.Metadata["foo"] != "bar" {
|
||||
t.Error("not expected metadata")
|
||||
}
|
||||
}
|
||||
|
80
errors/types.go
Normal file
80
errors/types.go
Normal file
@ -0,0 +1,80 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
// BadRequest new BadRequest error that is mapped to a 400 response.
|
||||
func BadRequest(reason, message string) *Error {
|
||||
return New(http.StatusBadRequest, reason, message)
|
||||
}
|
||||
|
||||
// IsBadRequest determines if err is an error which indicates a BadRequest error.
|
||||
// It supports wrapped errors.
|
||||
func IsBadRequest(err error) bool {
|
||||
return Code(err) == http.StatusBadRequest
|
||||
}
|
||||
|
||||
// Unauthorized new Unauthorized error that is mapped to a 401 response.
|
||||
func Unauthorized(reason, message string) *Error {
|
||||
return New(http.StatusUnauthorized, reason, message)
|
||||
}
|
||||
|
||||
// IsUnauthorized determines if err is an error which indicates a Unauthorized error.
|
||||
// It supports wrapped errors.
|
||||
func IsUnauthorized(err error) bool {
|
||||
return Code(err) == http.StatusUnauthorized
|
||||
}
|
||||
|
||||
// Forbidden new Forbidden error that is mapped to a 403 response.
|
||||
func Forbidden(reason, message string) *Error {
|
||||
return New(http.StatusForbidden, reason, message)
|
||||
}
|
||||
|
||||
// IsForbidden determines if err is an error which indicates a Forbidden error.
|
||||
// It supports wrapped errors.
|
||||
func IsForbidden(err error) bool {
|
||||
return Code(err) == http.StatusForbidden
|
||||
}
|
||||
|
||||
// NotFound new NotFound error that is mapped to a 404 response.
|
||||
func NotFound(reason, message string) *Error {
|
||||
return New(http.StatusNotFound, reason, message)
|
||||
}
|
||||
|
||||
// IsNotFound determines if err is an error which indicates an NotFound error.
|
||||
// It supports wrapped errors.
|
||||
func IsNotFound(err error) bool {
|
||||
return Code(err) == http.StatusNotFound
|
||||
}
|
||||
|
||||
// Conflict new Conflict error that is mapped to a 409 response.
|
||||
func Conflict(reason, message string) *Error {
|
||||
return New(http.StatusConflict, reason, message)
|
||||
}
|
||||
|
||||
// IsConflict determines if err is an error which indicates a Conflict error.
|
||||
// It supports wrapped errors.
|
||||
func IsConflict(err error) bool {
|
||||
return Code(err) == http.StatusConflict
|
||||
}
|
||||
|
||||
// InternalServer new InternalServer error that is mapped to a 500 response.
|
||||
func InternalServer(reason, message string) *Error {
|
||||
return New(http.StatusInternalServerError, reason, message)
|
||||
}
|
||||
|
||||
// IsInternalServer determines if err is an error which indicates an InternalServer error.
|
||||
// It supports wrapped errors.
|
||||
func IsInternalServer(err error) bool {
|
||||
return Code(err) == http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// ServiceUnavailable new ServiceUnavailable error that is mapped to a HTTP 503 response.
|
||||
func ServiceUnavailable(reason, message string) *Error {
|
||||
return New(http.StatusServiceUnavailable, reason, message)
|
||||
}
|
||||
|
||||
// IsServiceUnavailable determines if err is an error which indicates a ServiceUnavailable error.
|
||||
// It supports wrapped errors.
|
||||
func IsServiceUnavailable(err error) bool {
|
||||
return Code(err) == http.StatusServiceUnavailable
|
||||
}
|
32
errors/types_test.go
Normal file
32
errors/types_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package errors
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
var (
|
||||
input = []*Error{
|
||||
BadRequest("reason_400", "message_400"),
|
||||
Unauthorized("reason_401", "message_401"),
|
||||
Forbidden("reason_403", "message_403"),
|
||||
NotFound("reason_404", "message_404"),
|
||||
Conflict("reason_409", "message_409"),
|
||||
InternalServer("reason_500", "message_500"),
|
||||
ServiceUnavailable("reason_503", "message_503"),
|
||||
}
|
||||
output = []func(error) bool{
|
||||
IsBadRequest,
|
||||
IsUnauthorized,
|
||||
IsForbidden,
|
||||
IsNotFound,
|
||||
IsConflict,
|
||||
IsInternalServer,
|
||||
IsServiceUnavailable,
|
||||
}
|
||||
)
|
||||
|
||||
for i, in := range input {
|
||||
if !output[i](in) {
|
||||
t.Errorf("not expect: %v", in)
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ func Server(opts ...Option) middleware.Middleware {
|
||||
var (
|
||||
method string
|
||||
path string
|
||||
code int32
|
||||
code int
|
||||
)
|
||||
if info, ok := grpc.FromServerContext(ctx); ok {
|
||||
method = "POST"
|
||||
@ -90,7 +90,7 @@ func Client(opts ...Option) middleware.Middleware {
|
||||
var (
|
||||
method string
|
||||
path string
|
||||
code int32
|
||||
code int
|
||||
)
|
||||
if info, ok := grpc.FromClientContext(ctx); ok {
|
||||
method = "POST"
|
||||
|
@ -2,6 +2,7 @@ package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/errors"
|
||||
@ -39,7 +40,7 @@ func Recovery(opts ...Option) middleware.Middleware {
|
||||
options := options{
|
||||
logger: log.DefaultLogger,
|
||||
handler: func(ctx context.Context, req, err interface{}) error {
|
||||
return errors.Unknown("Unknown", "panic triggered: %v", err)
|
||||
return errors.InternalServer("recovery", fmt.Sprintf("panic triggered: %v", err))
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
|
@ -2,28 +2,38 @@ package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/errors"
|
||||
"github.com/go-kratos/kratos/v2/middleware"
|
||||
|
||||
//lint:ignore SA1019 grpc
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"google.golang.org/genproto/googleapis/rpc/errdetails"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type domainKey struct{}
|
||||
|
||||
// HandlerFunc is middleware error handler.
|
||||
type HandlerFunc func(error) error
|
||||
type HandlerFunc func(context.Context, error) error
|
||||
|
||||
// Option is recovery option.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
domain string
|
||||
handler HandlerFunc
|
||||
}
|
||||
|
||||
// WithDomain with service domain.
|
||||
func WithDomain(domain string) Option {
|
||||
return func(o *options) {
|
||||
o.domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
// WithHandler with status handler.
|
||||
func WithHandler(h HandlerFunc) Option {
|
||||
return func(o *options) {
|
||||
@ -34,7 +44,7 @@ func WithHandler(h HandlerFunc) Option {
|
||||
// Server is an error middleware.
|
||||
func Server(opts ...Option) middleware.Middleware {
|
||||
options := options{
|
||||
handler: errorEncode,
|
||||
handler: encodeErr,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
@ -43,7 +53,8 @@ func Server(opts ...Option) middleware.Middleware {
|
||||
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
reply, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
return nil, options.handler(err)
|
||||
ctx = context.WithValue(ctx, domainKey{}, options.domain)
|
||||
return nil, options.handler(ctx, err)
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
@ -53,7 +64,7 @@ func Server(opts ...Option) middleware.Middleware {
|
||||
// Client is an error middleware.
|
||||
func Client(opts ...Option) middleware.Middleware {
|
||||
options := options{
|
||||
handler: errorDecode,
|
||||
handler: decodeErr,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
@ -62,35 +73,26 @@ func Client(opts ...Option) middleware.Middleware {
|
||||
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
reply, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
return nil, options.handler(err)
|
||||
return nil, options.handler(ctx, err)
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errorEncode(err error) error {
|
||||
se, ok := errors.FromError(err)
|
||||
if !ok {
|
||||
se = &errors.StatusError{
|
||||
Code: 2,
|
||||
Message: err.Error(),
|
||||
}
|
||||
func encodeErr(ctx context.Context, err error) error {
|
||||
se := errors.FromError(err)
|
||||
if se.Domain == "" {
|
||||
se.Domain, _ = ctx.Value(domainKey{}).(string)
|
||||
}
|
||||
gs := status.Newf(codes.Code(se.Code), "%s: %s", se.Reason, se.Message)
|
||||
gs := status.Newf(httpToGRPCCode(se.Code), "%s: %s", se.Reason, se.Message)
|
||||
details := []proto.Message{
|
||||
&errdetails.ErrorInfo{
|
||||
Domain: se.Domain,
|
||||
Reason: se.Reason,
|
||||
Metadata: map[string]string{"message": se.Message},
|
||||
Metadata: se.Metadata,
|
||||
},
|
||||
}
|
||||
for _, any := range se.Details {
|
||||
detail := &ptypes.DynamicAny{}
|
||||
if err := ptypes.UnmarshalAny(any, detail); err != nil {
|
||||
continue
|
||||
}
|
||||
details = append(details, detail.Message)
|
||||
}
|
||||
gs, err = gs.WithDetails(details...)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -98,19 +100,60 @@ func errorEncode(err error) error {
|
||||
return gs.Err()
|
||||
}
|
||||
|
||||
func errorDecode(err error) error {
|
||||
func decodeErr(ctx context.Context, err error) error {
|
||||
gs := status.Convert(err)
|
||||
se := &errors.StatusError{
|
||||
Code: int32(gs.Code()),
|
||||
Details: gs.Proto().Details,
|
||||
se := &errors.Error{
|
||||
Code: grpcToHTTPCode(gs.Code()),
|
||||
Message: gs.Message(),
|
||||
}
|
||||
for _, detail := range gs.Details() {
|
||||
switch d := detail.(type) {
|
||||
case *errdetails.ErrorInfo:
|
||||
se.Domain = d.Domain
|
||||
se.Reason = d.Reason
|
||||
se.Message = d.Metadata["message"]
|
||||
se.Metadata = d.Metadata
|
||||
return se
|
||||
}
|
||||
}
|
||||
return se
|
||||
}
|
||||
|
||||
func httpToGRPCCode(code int) codes.Code {
|
||||
switch code {
|
||||
case http.StatusBadRequest:
|
||||
return codes.InvalidArgument
|
||||
case http.StatusUnauthorized:
|
||||
return codes.Unauthenticated
|
||||
case http.StatusForbidden:
|
||||
return codes.PermissionDenied
|
||||
case http.StatusNotFound:
|
||||
return codes.NotFound
|
||||
case http.StatusConflict:
|
||||
return codes.Aborted
|
||||
case http.StatusInternalServerError:
|
||||
return codes.Internal
|
||||
case http.StatusServiceUnavailable:
|
||||
return codes.Unavailable
|
||||
}
|
||||
return codes.Unknown
|
||||
}
|
||||
|
||||
func grpcToHTTPCode(code codes.Code) int {
|
||||
switch code {
|
||||
case codes.InvalidArgument:
|
||||
return http.StatusBadRequest
|
||||
case codes.Unauthenticated:
|
||||
return http.StatusUnauthorized
|
||||
case codes.PermissionDenied:
|
||||
return http.StatusForbidden
|
||||
case codes.NotFound:
|
||||
return http.StatusNotFound
|
||||
case codes.Aborted:
|
||||
return http.StatusConflict
|
||||
case codes.Internal:
|
||||
return http.StatusInternalServerError
|
||||
case codes.Unavailable:
|
||||
return http.StatusServiceUnavailable
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/errors"
|
||||
@ -9,13 +10,13 @@ import (
|
||||
)
|
||||
|
||||
func TestErrEncoder(t *testing.T) {
|
||||
err := errors.InvalidArgument("InvalidArgument", "format")
|
||||
en := errorEncode(err)
|
||||
err := errors.BadRequest("InvalidArgument", "format")
|
||||
en := encodeErr(context.Background(), err)
|
||||
if code := status.Code(en); code != codes.InvalidArgument {
|
||||
t.Errorf("expected %d got %d", codes.InvalidArgument, code)
|
||||
}
|
||||
de := errorDecode(en)
|
||||
if !errors.IsInvalidArgument(de) {
|
||||
de := decodeErr(context.Background(), en)
|
||||
if !errors.IsBadRequest(de) {
|
||||
t.Errorf("expected %v got %v", err, de)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func Validator() middleware.Middleware {
|
||||
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
|
||||
if v, ok := req.(validator); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return nil, errors.InvalidArgument("Validator", err.Error())
|
||||
return nil, errors.BadRequest("validator", err.Error())
|
||||
}
|
||||
}
|
||||
return handler(ctx, req)
|
||||
|
@ -117,7 +117,7 @@ func Do(client *http.Client, req *http.Request, target interface{}) error {
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
se := &errors.StatusError{Code: 2}
|
||||
se := &errors.Error{Code: 500}
|
||||
if err := decodeResponse(res, se); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -108,17 +108,11 @@ func encodeResponse(w http.ResponseWriter, r *http.Request, v interface{}) error
|
||||
|
||||
// encodeError encodes the error to the HTTP response.
|
||||
func encodeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
se, ok := errors.FromError(err)
|
||||
if !ok {
|
||||
se = &errors.StatusError{
|
||||
Code: 2,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
se := errors.FromError(err)
|
||||
codec := codecForRequest(r)
|
||||
data, _ := codec.Marshal(se)
|
||||
w.Header().Set(contentTypeHeader, contentType(codec.Name()))
|
||||
w.WriteHeader(se.HTTPStatus())
|
||||
w.WriteHeader(se.Code)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user