Jak zarządzać wyjątkami rzucanymi w filtrach na wiosnę?


109

Chcę użyć ogólnego sposobu zarządzania kodami błędów 5xx, powiedzmy konkretnie w przypadku, gdy db nie działa w całej mojej wiosennej aplikacji. Chcę mieć ładny plik JSON o błędzie zamiast śladu stosu.

Dla kontrolerów mam @ControllerAdviceklasę dla różnych wyjątków i to również wychwytuje przypadek, że db zatrzymuje się w środku żądania. Ale to nie wszystko. Zdarza mi się również mieć niestandardowe CorsFilterrozszerzenie OncePerRequestFilteri kiedy dzwonię, doFilterotrzymuję CannotGetJdbcConnectionExceptioni nie będzie zarządzany przez @ControllerAdvice. Przeczytałem kilka rzeczy w Internecie, które tylko wprawiły mnie w zakłopotanie.

Mam więc dużo pytań:

  • Czy muszę zaimplementować niestandardowy filtr? Znalazłem, ExceptionTranslationFilterale to tylko obsługuje AuthenticationExceptionlub AccessDeniedException.
  • Myślałem o wdrożeniu własnego HandlerExceptionResolver, ale to wzbudziło we mnie wątpliwości, nie mam żadnego niestandardowego wyjątku do zarządzania, musi być bardziej oczywisty sposób niż ten. Próbowałem też dodać try / catch i wywołać implementację HandlerExceptionResolver(powinno być wystarczająco dobre, mój wyjątek nie jest niczym specjalnym), ale to nie zwraca niczego w odpowiedzi, otrzymuję status 200 i puste ciało.

Czy jest jakiś dobry sposób, aby sobie z tym poradzić? Dzięki


Możemy zastąpić BasicErrorController Spring Boot. Napisałem o tym na blogu tutaj: naturalprogrammer.com/blog/1685463/…
Sanjay

Odpowiedzi:


86

Oto co zrobiłem:

Czytałem podstawowe informacje na temat filtrów tutaj i zorientowali się, że trzeba utworzyć własny filtr, który będzie pierwszym w łańcuchu filtrów i będzie mieć zaczep Spróbuj złapać wszystkie wyjątki uruchomieniowe, które mogą tam wystąpić. Następnie muszę ręcznie utworzyć plik json i umieścić go w odpowiedzi.

Oto mój niestandardowy filtr:

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

Następnie dodałem go w pliku web.xml przed rozszerzeniem CorsFilter. I to działa!

<filter> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class> 
</filter> 


<filter-mapping> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

<filter> 
    <filter-name>CorsFilter</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter> 

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Czy możesz opublikować swoją klasę ErrorResponse?
Shiva kumar

1
Klasa @Shivakumar ErrorResponse jest prawdopodobnie prostym DTO z prostymi właściwościami kodu / wiadomości.
ratijas

23

Chciałem przedstawić rozwiązanie oparte na odpowiedzi @kopelitsa . Główne różnice to:

  1. Ponowne użycie obsługi wyjątków kontrolera przy użyciu HandlerExceptionResolver.
  2. Korzystanie z konfiguracji Java zamiast konfiguracji XML

Najpierw musisz upewnić się, że masz klasę, która obsługuje wyjątki występujące w zwykłym RestController / Controller (klasa z adnotacją @RestControllerAdvicelub @ControllerAdvicei metody z adnotacjami @ExceptionHandler). To obsługuje wyjątki występujące w kontrolerze. Oto przykład użycia RestControllerAdvice:

@RestControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorDTO processRuntimeException(RuntimeException e) {
        return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
    }

    private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
        (...)
    }
}

Aby ponownie użyć tego zachowania w łańcuchu filtrów Spring Security, musisz zdefiniować filtr i podłączyć go do konfiguracji zabezpieczeń. Filtr musi przekierować wyjątek do wyżej zdefiniowanej obsługi wyjątków. Oto przykład:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

Utworzony filtr należy następnie dodać do SecurityConfiguration. Musisz podłączyć go do łańcucha bardzo wcześnie, ponieważ wszystkie poprzednie wyjątki filtru nie zostaną przechwycone. W moim przypadku rozsądne było dodanie go przed LogoutFilter. Zobacz domyślny łańcuch filtrów i jego kolejność w oficjalnych dokumentach . Oto przykład:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
            (...)
    }

}

19

Sam natknąłem się na ten problem i wykonałem poniższe kroki, aby ponownie użyć mojego, ExceptionControllerktóry jest oznaczony adnotacją @ControllerAdvisedo Exceptionswrzucenia do zarejestrowanego filtru.

Jest oczywiście wiele sposobów obsługi wyjątków, ale w moim przypadku chciałem, aby wyjątek był obsługiwany przez mój, ExceptionControllerponieważ jestem uparty, a także dlatego, że nie chcę kopiować / wklejać tego samego kodu (tj. Mam trochę przetwarzania / logowania kod w ExceptionController). Chciałbym zwrócić piękną JSONodpowiedź, tak jak pozostałe wyjątki wyrzucone nie z filtra.

{
  "status": 400,
  "message": "some exception thrown when executing the request"
}

W każdym razie udało mi się wykorzystać moje ExceptionHandleri musiałem zrobić trochę więcej, jak pokazano poniżej w krokach:

Kroki


  1. Masz niestandardowy filtr, który może zgłosić wyjątek lub nie
  2. Masz kontroler Spring, który obsługuje wyjątki przy użyciu @ControllerAdvisenp. MyExceptionController

Przykładowy kod

//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
   //Spring Controller annotated with @ControllerAdvise which has handlers
   //for exceptions
   private MyExceptionController myExceptionController; 

   @Override
   public void destroy() {
        // TODO Auto-generated method stub
   }

   @Override
   public void init(FilterConfig arg0) throws ServletException {
       //Manually get an instance of MyExceptionController
       ApplicationContext ctx = WebApplicationContextUtils
                  .getRequiredWebApplicationContext(arg0.getServletContext());

       //MyExceptionHanlder is now accessible because I loaded it manually
       this.myExceptionController = ctx.getBean(MyExceptionController.class); 
   }

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
           //code that throws exception
        } catch(Exception ex) {
          //MyObject is whatever the output of the below method
          MyObject errorDTO = myExceptionController.handleMyException(req, ex); 

          //set the response object
          res.setStatus(errorDTO .getStatus());
          res.setContentType("application/json");

          //pass down the actual obj that exception handler normally send
          ObjectMapper mapper = new ObjectMapper();
          PrintWriter out = res.getWriter(); 
          out.print(mapper.writeValueAsString(errorDTO ));
          out.flush();

          return; 
        }

        //proceed normally otherwise
        chain.doFilter(request, response); 
     }
}

A teraz przykładowy kontroler sprężyny, który obsługuje Exceptionw normalnych przypadkach (tj. Wyjątki, które zwykle nie są zgłaszane na poziomie filtru, ten, którego chcemy użyć do wyjątków rzucanych w filtrze)

//sample SpringController 
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {

    //sample handler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLException.class)
    public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
            Exception ex){
        ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
                + "executing the request."); 
        return response;
    }
    //other handlers
}

Udostępnianie rozwiązania tym, którzy chcą użyć ExceptionControllerdo Exceptionswrzucenia do filtra.


10
Cóż, możesz podzielić się swoim własnym rozwiązaniem, które brzmi tak, jak to zrobić :)
Raf

1
Jeśli chcesz uniknąć podłączenia kontrolera do twojego filtra (co jest tym, do czego odnosi się @ Bato-BairTsyrenov, jak zakładam), możesz łatwo wyodrębnić logikę, w której tworzysz ErrorDTO do własnej @Componentklasy i użyj jej w filtrze i w Administratora.
Rüdiger Schulz

1
Nie do końca się z tobą zgadzam, bo to nie jest zbyt czyste wtryskiwanie konkretnego kontrolera do twojego filtra.
psv

Jak wspomniano w answertym, jest to jeden ze sposobów! Nie twierdziłem, że to najlepszy sposób. Dziękuję za podzielenie się troską @psv Jestem pewien, że społeczność doceni rozwiązanie, o którym myślisz :)
Raf

12

Oto, co zrobiłem, opierając się na połączeniu powyższych odpowiedzi ... Mieliśmy już GlobalExceptionHandleradnotację @ControllerAdvicei chciałem również znaleźć sposób na ponowne użycie tego kodu do obsługi wyjątków pochodzących z filtrów.

Najprostszym rozwiązaniem, jakie udało mi się znaleźć, było pozostawienie obsługi wyjątków w spokoju i zaimplementowanie kontrolera błędów w następujący sposób:

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

Tak więc wszelkie błędy spowodowane przez wyjątki najpierw przechodzą przez program ErrorControlleri są przekierowywane do programu obsługi wyjątków przez ponowne zgłoszenie ich z @Controllerkontekstu, podczas gdy wszelkie inne błędy (nie spowodowane bezpośrednio przez wyjątek) przechodzą ErrorControllerbez modyfikacji.

Jakieś powody, dla których to właściwie zły pomysł?


1
Dzięki teraz testuję to rozwiązanie, ale w moim przypadku działa idealnie.
Maciej

czysty i prosty dodatek do buta wiosennego 2.0+, który należy dodać @Override public String getErrorPath() { return null; }
Fma

możesz użyć javax.servlet.RequestDispatcher.ERROR_EXCEPTION zamiast „javax.servlet.error.exception”
Marx

9

Jeśli chcesz uzyskać ogólny sposób, możesz zdefiniować stronę błędu w web.xml:

<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/500</location>
</error-page>

I dodaj mapowanie w Spring MVC:

@Controller
public class ErrorController {

    @RequestMapping(value="/500")
    public @ResponseBody String handleException(HttpServletRequest req) {
        // you can get the exception thrown
        Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");

        // customize response to what you want
        return "Internal server error.";
    }
}

Ale w reszcie api przekierowanie z lokalizacją nie jest dobrym rozwiązaniem.
jmattheis

@jmattheis Powyższe nie jest przekierowaniem.
holmis83

To prawda, widziałem lokalizację i pomyślałem, że ma coś do zrobienia z lokalizacją http. W takim razie tego potrzebuję (:
jmattheis

Czy możesz dodać odpowiednik konfiguracji Java do pliku web.xml, jeśli taki istnieje?
k-den

1
@ k-den Myślę, że w obecnej specyfikacji nie ma odpowiednika konfiguracji Java, ale można mieszać konfigurację web.xml i Java.
holmis83

5

To jest moje rozwiązanie przez nadpisanie domyślnej obsługi Spring Boot / Error

package com.mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * This controller is vital in order to handle exceptions thrown in Filters.
 */
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {

    private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);

    private final ErrorAttributes errorAttributes;

    @Autowired
    public ErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
        Map<String, Object> result =     this.errorAttributes.getErrorAttributes(requestAttributes, false);

        Throwable error = this.errorAttributes.getError(requestAttributes);

        ResponseStatus annotation =     AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
        HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;

        result.put("status", statusCode.value());
        result.put("error", statusCode.getReasonPhrase());

        LOGGER.error(result.toString());
        return new ResponseEntity<>(result, statusCode) ;
    }

}

czy ma to wpływ na jakąkolwiek automatyczną konfigurację?
Samet Baskıcı

Należy zauważyć, że HandlerExceptionResolver niekoniecznie obsługuje wyjątek. Może więc przejść jako HTTP 200. Użycie response.setStatus (..) przed wywołaniem wydaje się bezpieczniejsze.
ThomasRS

5

Tylko w celu uzupełnienia innych dobrych odpowiedzi, ponieważ zbyt niedawno chciałem pojedynczy składnik obsługi błędów / wyjątków w prostej aplikacji SpringBoot zawierającej filtry, które mogą generować wyjątki, z innymi wyjątkami potencjalnie wyrzucanymi z metod kontrolera.

Na szczęście wydaje się, że nic nie stoi na przeszkodzie, aby połączyć porady dotyczące kontrolera z zastąpieniem domyślnej obsługi błędów Springa w celu zapewnienia spójnych ładunków odpowiedzi, umożliwienia udostępniania logiki, sprawdzania wyjątków z filtrów, pułapek wyjątków zgłaszanych przez usługi itp.

Na przykład


@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(ValidationException.class)
  public Error handleValidationException(
      final ValidationException validationException) {
    return new Error("400", "Incorrect params"); // whatever
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error handleUnknownException(final Exception exception) {
    return new Error("500", "Unexpected error processing request");
  }

  @RequestMapping("/error")
  public ResponseEntity handleError(final HttpServletRequest request,
      final HttpServletResponse response) {

    Object exception = request.getAttribute("javax.servlet.error.exception");

    // TODO: Logic to inspect exception thrown from Filters...
    return ResponseEntity.badRequest().body(new Error(/* whatever */));
  }

  @Override
  public String getErrorPath() {
    return "/error";
  }

}

3

Jeśli chcesz przetestować stan aplikacji iw przypadku problemu zwrócić błąd HTTP, proponuję filtr. Poniższy filtr obsługuje wszystkie żądania HTTP. Najkrótsze rozwiązanie w Spring Boot z filtrem javax.

W realizacji mogą być różne warunki. W moim przypadku applicationManager sprawdza, czy aplikacja jest gotowa.

import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SystemIsReadyFilter implements Filter {

    @Autowired
    private ApplicationManager applicationManager;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!applicationManager.isApplicationReady()) {
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}

2

Po zapoznaniu się z różnymi metodami sugerowanymi w powyższych odpowiedziach zdecydowałem się obsłużyć wyjątki uwierzytelniania za pomocą niestandardowego filtru. Byłem w stanie obsłużyć status odpowiedzi i kody za pomocą klasy odpowiedzi na błąd przy użyciu następującej metody.

Utworzyłem niestandardowy filtr i zmodyfikowałem konfigurację zabezpieczeń za pomocą metody addFilterAfter i dodałem po klasie CorsFilter.

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //Cast the servlet request and response to HttpServletRequest and HttpServletResponse
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    // Grab the exception from the request attribute
    Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
    //Set response content type to application/json
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

    //check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
    if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
        ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(convertObjectToJson(errorResponse));
        writer.flush();
        return;
    }
    // If exception instance cannot be determined, then throw a nice exception and desired response code.
    else if(exception!=null){
            ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(convertObjectToJson(errorResponse));
            writer.flush();
            return;
        }
        else {
        // proceed with the initial request if no exception is thrown.
            chain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

public String convertObjectToJson(Object object) throws JsonProcessingException {
    if (object == null) {
        return null;
    }
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
}
}

SecurityConfig klasa

    @Configuration
    public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AuthFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
                .cors(); //........
        return http;
     }
   }

Klasa ErrorResponse

public class ErrorResponse  {
private final String message;
private final String description;

public ErrorResponse(String description, String message) {
    this.message = message;
    this.description = description;
}

public String getMessage() {
    return message;
}

public String getDescription() {
    return description;
}}

0

Możesz użyć następującej metody wewnątrz bloku catch:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

Zwróć uwagę, że możesz użyć dowolnego kodu HttpStatus i niestandardowej wiadomości.


-1

To dziwne, ponieważ @ControllerAdvice powinno działać, czy wychwytujesz prawidłowy wyjątek?

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = DataAccessException.class)
    public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
       response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
       //Json return
    }
}

Spróbuj też złapać ten wyjątek w CorsFilter i wysłać błąd 500, coś takiego

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}

Obsługa wyjątku w CorsFilter działa, ale nie jest zbyt czysta. Właściwie to, czego naprawdę potrzebuję, to obsługa wyjątku dla wszystkich filtrów
kopelitsa

35
Rzut wyjątkowy z Filtermoże nie zostać złapany, @ControllerAdviceponieważ może nie sięgać DispatcherServlet.
Thanh Nguyen Van

-1

Nie musisz w tym celu tworzyć niestandardowego filtru. Rozwiązaliśmy to, tworząc niestandardowe wyjątki, które rozszerzają ServletException (który jest wyrzucany z metody doFilter, pokazanej w deklaracji). Są one następnie przechwytywane i obsługiwane przez naszą globalną procedurę obsługi błędów.

edycja: gramatyka


Czy możesz udostępnić fragment kodu swojego globalnego modułu obsługi błędów?
Neeraj Vernekar

to nie działa dla mnie. Zrobiłem niestandardowy wyjątek, który rozszerza ServletException, dodałem obsługę tego wyjątku w ExceptionHandler, ale nie został tam przechwycony.
Marx
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.