Najlepsza praktyka zwracania błędów w interfejsie API sieci Web ASP.NET


384

Mam wątpliwości dotyczące sposobu, w jaki zwracamy błędy klientowi.

Czy zwracamy błąd natychmiast, zgłaszając wyjątek HttpResponseException, gdy pojawia się błąd:

public void Post(Customer customer)
{
    if (string.IsNullOrEmpty(customer.Name))
    {
        throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) 
    }
    if (customer.Accounts.Count == 0)
    {
         throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest) 
    }
}

Lub gromadzimy wszystkie błędy, a następnie odsyłamy do klienta:

public void Post(Customer customer)
{
    List<string> errors = new List<string>();
    if (string.IsNullOrEmpty(customer.Name))
    {
        errors.Add("Customer Name cannot be empty"); 
    }
    if (customer.Accounts.Count == 0)
    {
         errors.Add("Customer does not have any account"); 
    }
    var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest);
    throw new HttpResponseException(responseMessage);
}

To tylko przykładowy kod, nie ma znaczenia ani błąd sprawdzania poprawności, ani błąd serwera, chciałbym tylko poznać najlepsze praktyki, zalety i wady każdego podejścia.


1
Zobacz stackoverflow.com/a/22163675/200442, którego powinieneś używać ModelState.
Daniel Little

1
Zauważ, że odpowiedzi tutaj dotyczą tylko wyjątków zgłaszanych w samym kontrolerze. Jeśli interfejs API zwraca IQueryable <Model>, który nie został jeszcze wykonany, wyjątek nie znajduje się w kontrolerze i nie został przechwycony ...
Jess

3
Bardzo fajne pytanie, ale jakoś nie dostaję przeciążenia konstruktora przez HttpResponseExceptionklasę, która bierze dwa parametry wymienione w twoim poście - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) tj.HttpResponseException(string, HttpStatusCode)
RBT

Odpowiedzi:


293

Dla mnie zazwyczaj wysyłam zwrot HttpResponseExceptioni ustawiam kod statusu odpowiednio w zależności od zgłoszonego wyjątku, a jeśli wyjątek jest krytyczny, czy nie, zdecyduję, czy od HttpResponseExceptionrazu go odesłam.

Pod koniec dnia jest to interfejs API wysyłający odpowiedzi, a nie widoki, więc myślę, że w porządku jest odesłanie konsumentowi wiadomości z wyjątkiem i kodem statusu. Obecnie nie muszę kumulować błędów i odsyłać je z powrotem, ponieważ większość wyjątków wynika zwykle z niepoprawnych parametrów lub połączeń itp.

Przykładem w mojej aplikacji jest to, że czasami klient prosi o dane, ale dane nie są dostępne, więc rzucam niestandardowy NoDataAvailableException i pozwalam na bąbelkowanie do aplikacji interfejsu API sieci Web, gdzie następnie w moim niestandardowym filtrze, który przechwytuje odesłanie odpowiedni komunikat wraz z poprawnym kodem statusu.

Nie jestem w 100% pewien, jaka jest najlepsza praktyka w tym zakresie, ale obecnie działa to dla mnie, więc to właśnie robię.

Aktualizacja :

Od kiedy odpowiedziałem na to pytanie, na ten temat napisano kilka postów na blogu:

https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling

(ten ma kilka nowych funkcji w nocnych kompilacjach) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Aktualizacja 2

Zaktualizuj nasz proces obsługi błędów, mamy dwa przypadki:

  1. W przypadku ogólnych błędów, takich jak nie znaleziono lub niepoprawnych parametrów przekazywanych do akcji zwracamy a, HttpResponseExceptionaby natychmiast przerwać przetwarzanie. Dodatkowo za błędy modelu w naszych działaniach przekażemy słownik stanu modelu Request.CreateErrorResponserozszerzeniu i zawiniemy go w HttpResponseException. Dodanie słownika stanu modelu powoduje wyświetlenie listy błędów modelu wysłanych w treści odpowiedzi.

  2. W przypadku błędów, które występują w wyższych warstwach, błędów serwera, pozwalamy wyjątkowi na bąbelkowanie aplikacji Web API, tutaj mamy globalny filtr wyjątków, który patrzy na wyjątek, rejestruje go za pomocą ELMAH i próbuje znaleźć sens, ustawiając poprawny HTTP kod statusu i odpowiedni przyjazny komunikat o błędzie jako treść ponownie w HttpResponseException. Z wyjątkami, których nie oczekujemy, klient otrzyma domyślny błąd wewnętrzny serwera 500, ale ogólny komunikat ze względów bezpieczeństwa.

Aktualizacja 3

Ostatnio, po podniesieniu Web API 2, do odesłania ogólnych błędów używamy teraz interfejsu IHttpActionResult , a konkretnie wbudowanych klas w System.Web.Http.Resultsprzestrzeni nazw, takich jak NotFound, BadRequest, gdy pasują, jeśli nie rozszerzamy ich, na przykład wynik NotFound z komunikatem odpowiedzi:

public class NotFoundWithMessageResult : IHttpActionResult
{
    private string message;

    public NotFoundWithMessageResult(string message)
    {
        this.message = message;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotFound);
        response.Content = new StringContent(message);
        return Task.FromResult(response);
    }
}

Dziękuję za odpowiedź geepie, to dobre doświadczenie, więc wolisz natychmiast wysłać expcetion?
cuongle

Jak powiedziałem, to naprawdę zależy od wyjątku. Fatalny wyjątek, taki jak na przykład użytkownik przekazuje interfejsowi WWW niepoprawny parametr do punktu końcowego, a następnie utworzę wyjątek HttpResponseException i od razu zwrócę go do używającej aplikacji.
gdp

Wyjątki w pytaniu są tak naprawdę bardziej na temat sprawdzania poprawności, patrz stackoverflow.com/a/22163675/200442 .
Daniel Little

1
@DanielLittle Przeanalizuj jego pytanie. Cytuję: „To tylko przykładowy kod, nie ma znaczenia ani błąd sprawdzania poprawności, ani błąd serwera”
gdp

@gdp Mimo to tak naprawdę są dwa składniki, sprawdzanie poprawności i wyjątki, więc najlepiej objąć oba.
Daniel Little

184

Interfejs API sieci Web ASP.NET 2 naprawdę go uprościł. Na przykład następujący kod:

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        HttpError err = new HttpError(message);
        return Request.CreateResponse(HttpStatusCode.NotFound, err);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

zwraca następującą treść do przeglądarki, gdy element nie zostanie znaleziony:

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT
Content-Length: 51

{
  "Message": "Product with id = 12 not found"
}

Sugestia: Nie zgłaszaj błędu HTTP 500, chyba że wystąpi błąd katastroficzny (na przykład wyjątek błędu WCF). Wybierz odpowiedni kod stanu HTTP, który reprezentuje stan twoich danych. (Zobacz poniższy link apigee.)

Spinki do mankietów:


4
Chciałbym pójść o krok dalej i rzucić ResourceNotFoundException z DAL / Repo, które sprawdzam w Web Api 2.2 ExceptionHandler dla typu ResourceNotFoundException, a następnie zwracam „Produkt o identyfikatorze xxx nie znaleziony”. W ten sposób jest ogólnie zakotwiczone w architekturze zamiast każdej akcji.
Pascal

1
Czy jest jakieś konkretne zastosowanie dla return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Czym jest różnica między CreateResponseiCreateErrorResponse
Zapnologica

10
Według w3.org/Protocols/rfc2616/rfc2616-sec10.html błąd klienta to kod o poziomie 400, a błąd serwera to kod o poziomie 500. Dlatego kod błędu 500 może być w wielu przypadkach bardzo odpowiedni dla interfejsu API sieci Web, a nie tylko „katastroficznych” błędów.
Jess

2
Musisz using System.Net.Http;wyświetlić CreateResponse()metodę rozszerzenia.
Adam Szabo

Nie podoba mi się w Request.CreateResponse () to, że zwraca niepotrzebne informacje o serializacji specyficzne dla Microsoft, takie jak „<string xmlns =" schemas.microsoft.com/2003/10/Serialization/ "> Mój błąd tutaj </ string > ”. W sytuacjach, gdy status 400 jest odpowiedni, stwierdziłem, że ApiController.BadRequest (komunikat ciągu) zwraca lepszy ciąg „<Error> <Message> Mój błąd tutaj </Message> </Error>”. Ale nie mogę znaleźć odpowiednika dla zwrócenia statusu 500 z prostym komunikatem.
vkelman

76

Wygląda na to, że masz więcej problemów z walidacją niż błędy / wyjątki, więc powiem trochę o obu.

Uprawomocnienie

Działania kontrolera powinny zasadniczo przyjmować modele wejściowe, w których sprawdzanie poprawności jest zadeklarowane bezpośrednio w modelu.

public class Customer
{ 
    [Require]
    public string Name { get; set; }
}

Następnie możesz użyć narzędzia, ActionFilterktóre automatycznie wysyła wiadomości sprawdzające poprawność z powrotem do klienta.

public class ValidationActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;

        if (!modelState.IsValid) {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
    }
} 

Aby uzyskać więcej informacji na ten temat, sprawdź http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Obsługa błędów

Najlepiej jest zwrócić klientowi wiadomość, która reprezentuje wyjątek, który się wydarzył (z odpowiednim kodem stanu).

Po wyjęciu z pudełka musisz użyć, Request.CreateErrorResponse(HttpStatusCode, message)jeśli chcesz określić wiadomość. Powoduje to jednak powiązanie kodu z Requestobiektem, co nie powinno być konieczne.

Zwykle tworzę własny typ „bezpiecznego” wyjątku, który, jak się spodziewam, wiedziałby, że klient będzie wiedział, jak obsłużyć i zawinąć wszystkie inne z ogólnym błędem 500.

Użycie filtra akcji do obsługi wyjątków wyglądałoby następująco:

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        var exception = context.Exception as ApiException;
        if (exception != null) {
            context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message);
        }
    }
}

Następnie możesz zarejestrować go globalnie.

GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());

To jest mój niestandardowy typ wyjątku.

using System;
using System.Net;

namespace WebApi
{
    public class ApiException : Exception
    {
        private readonly HttpStatusCode statusCode;

        public ApiException (HttpStatusCode statusCode, string message, Exception ex)
            : base(message, ex)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode, string message)
            : base(message)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode)
        {
            this.statusCode = statusCode;
        }

        public HttpStatusCode StatusCode
        {
            get { return this.statusCode; }
        }
    }
}

Przykład wyjątku, który może wygenerować mój interfejs API.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}

Mam problem związany z odpowiedzią na błąd w definicji klasy ApiExceptionFilterAttribute. W metodzie OnException wyjątek.StatusCode jest niepoprawny, ponieważ wyjątek stanowi wyjątek WebException. Co mogę zrobić w tym przypadku?
razp26

1
@ razp26, jeśli masz na myśli, var exception = context.Exception as WebException;że to była literówka, powinno byćApiException
Daniel Little

2
Czy możesz dodać przykład zastosowania klasy ApiExceptionFilterAttribute?
razp26

36

Możesz zgłosić wyjątek HttpResponseException

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);

23

W przypadku Web API 2 moje metody konsekwentnie zwracają IHttpActionResult, więc używam ...

public IHttpActionResult Save(MyEntity entity)
{
  ....

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

Ta odpowiedź jest w porządku, powinieneś jednak dodać odniesienie doSystem.Net.Http
Bellash

19

Jeśli korzystasz z ASP.NET Web API 2, najprostszym sposobem jest użycie krótkiej metody ApiController. Spowoduje to BadRequestResult.

return BadRequest("message");

Ściśle w przypadku błędów sprawdzania poprawności modelu używam przeciążenia BadRequest (), które akceptuje obiekt ModelState:return BadRequest(ModelState);
timmi4sa 9.09.19

4

możesz użyć niestandardowego filtru ActionFilter w interfejsie WWW, aby sprawdzić poprawność modelu

public class DRFValidationFilters : ActionFilterAttribute
{

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

            //BadRequest(actionContext.ModelState);
        }
    }
    public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {

        return Task.Factory.StartNew(() => {

            if (!actionContext.ModelState.IsValid)
            {
                actionContext.Response = actionContext.Request
                     .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);                    
            }
        });

    }

public class AspirantModel
{
    public int AspirantId { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }        
    public string LastName { get; set; }
    public string AspirantType { get; set; }       
    [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$", ErrorMessage = "Not a valid Phone number")]
    public string MobileNumber { get; set; }
    public int StateId { get; set; }
    public int CityId { get; set; }
    public int CenterId { get; set; }

}

    [HttpPost]
    [Route("AspirantCreate")]
    [DRFValidationFilters]
    public IHttpActionResult Create(AspirantModel aspirant)
    {
            if (aspirant != null)
            {

            }
            else
            {
                return Conflict();
            }
          return Ok();

}

Zarejestruj klasę CustomAttribute w webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());


4

Opierając się na Manish Jainodpowiedzi (która jest przeznaczona dla Web API 2, który upraszcza rzeczy):

1) Użyj struktur sprawdzania poprawności, aby zareagować na jak najwięcej błędów sprawdzania poprawności. Struktury te można również wykorzystać do odpowiedzi na żądania pochodzące z formularzy.

public class FieldError
{
    public String FieldName { get; set; }
    public String FieldMessage { get; set; }
}

// a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.)
public class ValidationResult<T>
{
    public bool IsError { get; set; }

    /// <summary>
    /// validation message. It is used as a success message if IsError is false, otherwise it is an error message
    /// </summary>
    public string Message { get; set; } = string.Empty;

    public List<FieldError> FieldErrors { get; set; } = new List<FieldError>();

    public T Payload { get; set; }

    public void AddFieldError(string fieldName, string fieldMessage)
    {
        if (string.IsNullOrWhiteSpace(fieldName))
            throw new ArgumentException("Empty field name");

        if (string.IsNullOrWhiteSpace(fieldMessage))
            throw new ArgumentException("Empty field message");

        // appending error to existing one, if field already contains a message
        var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName));
        if (existingFieldError == null)
            FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage});
        else
            existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}";

        IsError = true;
    }

    public void AddEmptyFieldError(string fieldName, string contextInfo = null)
    {
        AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}");
    }
}

public class ValidationResult : ValidationResult<object>
{

}

2) Warstwa usługi zwróci ValidationResults, niezależnie od tego, czy operacja zakończy się powodzeniem, czy nie. Na przykład:

    public ValidationResult DoSomeAction(RequestFilters filters)
    {
        var ret = new ValidationResult();

        if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1));
        if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list");

        if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp));
        if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp));


        // validation affecting multiple input parameters
        if (filters.MinProp > filters.MaxProp)
        {
            ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop"));
            ret.AddFieldError(nameof(filters.MaxProp, "Check"));
        }

        // also specify a global error message, if we have at least one error
        if (ret.IsError)
        {
            ret.Message = "Failed to perform DoSomeAction";
            return ret;
        }

        ret.Message = "Successfully performed DoSomeAction";
        return ret;
    }

3) Kontroler API konstruuje odpowiedź na podstawie wyniku funkcji usługi

Jedną z opcji jest ustawienie praktycznie wszystkich parametrów jako opcjonalnych i przeprowadzenie niestandardowej weryfikacji, która zwraca bardziej znaczącą odpowiedź. Staram się również, aby żaden wyjątek nie wykraczał poza zakres usługi.

    [Route("DoSomeAction")]
    [HttpPost]
    public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null)
    {
        try
        {
            var filters = new RequestFilters 
            {
                SomeProp1 = someProp1 ,
                SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() ,
                MinProp = minProp, 
                MaxProp = maxProp
            };

            var result = theService.DoSomeAction(filters);
            return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result);
        }
        catch (Exception exc)
        {
            Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction");
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error"));
        }
    }

3

Użyj wbudowanej metody „InternalServerError” (dostępnej w ApiController):

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));

0

Wystarczy zaktualizować bieżący stan interfejsu WebAPI programu ASP.NET. Interfejs jest teraz wywoływany, IActionResulta implementacja niewiele się zmieniła:

[JsonObject(IsReference = true)]
public class DuplicateEntityException : IActionResult
{        
    public DuplicateEntityException(object duplicateEntity, object entityId)
    {
        this.EntityType = duplicateEntity.GetType().Name;
        this.EntityId = entityId;
    }

    /// <summary>
    ///     Id of the duplicate (new) entity
    /// </summary>
    public object EntityId { get; set; }

    /// <summary>
    ///     Type of the duplicate (new) entity
    /// </summary>
    public string EntityType { get; set; }

    public Task ExecuteResultAsync(ActionContext context)
    {
        var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database");

        var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message };

        return Task.FromResult(response);
    }

    #endregion
}

Wygląda to interesująco, ale gdzie konkretnie w projekcie umieszczony jest ten kod? Robię mój projekt Web API 2 w vb.net.
Off The Gold

To tylko model zwracania błędu i może znajdować się w dowolnym miejscu. Zwrócisz nową instancję powyższej klasy w swoim kontrolerze. Ale szczerze mówiąc, w miarę możliwości staram się korzystać z wbudowanych klas: this.Ok (), CreatedAtRoute (), NotFound (). Podpis tej metody to IHttpActionResult. Nie wiem, czy zmienili to wszystko za pomocą NetCore
Thomas Hagström,

-2

W przypadku błędów, w których modelstate.isvalid ma wartość false, generalnie wysyłam błąd, ponieważ jest generowany przez kod. Łatwo to zrozumieć dla programisty korzystającego z mojej usługi. Generalnie wysyłam wynik za pomocą poniższego kodu.

     if(!ModelState.IsValid) {
                List<string> errorlist=new List<string>();
                foreach (var value in ModelState.Values)
                {
                    foreach(var error in value.Errors)
                    errorlist.Add( error.Exception.ToString());
                    //errorlist.Add(value.Errors);
                }
                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);}

To wysyła błąd do klienta w poniższym formacie, który jest w zasadzie listą błędów:

    [  
    "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)",

       "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
    ]

Nie polecałbym odsyłania tego poziomu szczegółów w wyjątku, jeśli byłby to zewnętrzny interfejs API (tj. Udostępniony publicznie w Internecie). Powinieneś trochę popracować w filtrze i odesłać obiekt JSON (lub XML, jeśli jest to wybrany format), wyszczególniając błąd, a nie tylko wyjątek ToString.
Sudhanshu Mishra

Prawidłowe nie wysłano tego wyjątku dla zewnętrznego interfejsu API. Ale możesz go użyć do debugowania problemów w wewnętrznym interfejsie API i podczas testowania.
Ashish Sahu,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.