다음을 통해 공유


ASP.NET Web API HTTP 메시지 처리기

메시지 처리기는 HTTP 요청을 수신하고 HTTP 응답을 반환하는 클래스입니다. 메시지 처리기는 추상 HttpMessageHandler 클래스에서 파생됩니다.

일반적으로 일련의 메시지 처리기는 함께 연결됩니다. 첫 번째 처리기는 HTTP 요청을 수신하고 일부 처리를 수행하며 다음 처리기에 요청을 제공합니다. 어떤 시점에서 응답이 만들어지고 체인을 백업합니다. 이 패턴을 위임 처리기라고 합니다.

H T T P 요청을 수신하고 H T T P 응답을 반환하는 프로세스를 보여 주는 메시지 처리기가 함께 연결된 다이어그램

Server-Side 메시지 처리기

서버 쪽에서 Web API 파이프라인은 몇 가지 기본 제공 메시지 처리기를 사용합니다.

  • HttpServer 는 호스트에서 요청을 가져옵니다.
  • HttpRoutingDispatcher 는 경로에 따라 요청을 디스패치합니다.
  • HttpControllerDispatcher 는 Web API 컨트롤러에 요청을 보냅니다.

파이프라인에 사용자 지정 처리기를 추가할 수 있습니다. 메시지 처리기는 컨트롤러 작업이 아닌 HTTP 메시지 수준에서 작동하는 교차 절단 문제에 적합합니다. 예를 들어 메시지 처리기는 다음을 수행할 수 있습니다.

  • 요청 헤더를 읽거나 수정합니다.
  • 응답 헤더를 응답에 추가합니다.
  • 컨트롤러에 도달하기 전에 요청의 유효성을 검사합니다.

이 다이어그램은 파이프라인에 삽입된 두 개의 사용자 지정 처리기를 보여 줍니다.

Web A P I 파이프라인에 삽입된 두 개의 사용자 지정 처리기를 표시하는 서버 쪽 메시지 처리기의 다이어그램

참고

클라이언트 쪽에서 HttpClient는 메시지 처리기도 사용합니다. 자세한 내용은 HttpClient 메시지 처리기를 참조하세요.

사용자 지정 메시지 처리기

사용자 지정 메시지 처리기를 작성하려면 System.Net.Http.DelegatingHandler 에서 파생되고 SendAsync 메서드를 재정의합니다. 이 메서드의 서명은 다음과 같습니다.

Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken);

메서드는 HttpRequestMessage를 입력으로 사용하고 HttpResponseMessage를 비동기적으로 반환합니다. 일반적인 구현은 다음을 수행합니다.

  1. 요청 메시지를 처리합니다.
  2. 를 호출 base.SendAsync 하여 내부 처리기에 요청을 보냅니다.
  3. 내부 처리기는 응답 메시지를 반환합니다. (이 단계는 비동기입니다.)
  4. 응답을 처리하고 호출자에게 반환합니다.

다음은 간단한 예입니다.

public class MessageHandler1 : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("Process request");
        // Call the inner handler.
        var response = await base.SendAsync(request, cancellationToken);
        Debug.WriteLine("Process response");
        return response;
    }
}

참고

base.SendAsync에 대한 호출은 비동기입니다. 처리기가 이 호출 후 작업을 수행하는 경우 표시된 대로 await 키워드(keyword) 사용합니다.

위임 처리기는 내부 처리기를 건너뛰고 직접 응답을 만들 수도 있습니다.

public class MessageHandler2 : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Create the response.
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Hello!")
        };

        // Note: TaskCompletionSource creates a task that does not contain a delegate.
        var tsc = new TaskCompletionSource<HttpResponseMessage>();
        tsc.SetResult(response);   // Also sets the task state to "RanToCompletion"
        return tsc.Task;
    }
}

위임 처리기가 를 호출 base.SendAsync하지 않고 응답을 만드는 경우 요청은 파이프라인의 나머지 부분을 건너뜁니다. 이는 요청의 유효성을 검사하는 처리기에 유용할 수 있습니다(오류 응답 만들기).

기본 점 비동기 보내기를 호출하지 않고 응답을 만드는 프로세스를 보여 주는 사용자 지정 메시지 처리기의 다이어그램

파이프라인에 처리기 추가

서버 쪽에 메시지 처리기를 추가하려면 HttpConfiguration.MessageHandlers 컬렉션에 처리기를 추가합니다. "ASP.NET MVC 4 웹 애플리케이션" 템플릿을 사용하여 프로젝트를 만든 경우 WebApiConfig 클래스 내에서 이 작업을 수행할 수 있습니다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new MessageHandler1());
        config.MessageHandlers.Add(new MessageHandler2());

        // Other code not shown...
    }
}

메시지 처리기는 MessageHandlers 컬렉션에 표시되는 순서와 동일한 순서로 호출됩니다. 중첩되므로 응답 메시지는 다른 방향으로 이동합니다. 즉, 마지막 처리기는 응답 메시지를 가장 먼저 가져옵니다.

내부 처리기를 설정할 필요가 없습니다. Web API 프레임워크는 메시지 처리기를 자동으로 연결합니다.

자체 호스팅하는 경우 HttpSelfHostConfiguration 클래스의 instance 만들고 처리기를 MessageHandlers 컬렉션에 추가합니다.

var config = new HttpSelfHostConfiguration("http://localhost");
config.MessageHandlers.Add(new MessageHandler1());
config.MessageHandlers.Add(new MessageHandler2());

이제 사용자 지정 메시지 처리기의 몇 가지 예를 살펴보겠습니다.

예: X-HTTP-Method-Override

X-HTTP-메서드 재정의는 비표준 HTTP 헤더입니다. PUT 또는 DELETE와 같은 특정 HTTP 요청 형식을 보낼 수 없는 클라이언트를 위해 설계되었습니다. 대신 클라이언트는 POST 요청을 보내고 X-HTTP-Method-Override 헤더를 원하는 메서드로 설정합니다. 예:

X-HTTP-Method-Override: PUT

다음은 X-HTTP-메서드 재정의에 대한 지원을 추가하는 메시지 처리기입니다.

public class MethodOverrideHandler : DelegatingHandler      
{
    readonly string[] _methods = { "DELETE", "HEAD", "PUT" };
    const string _header = "X-HTTP-Method-Override";

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Check for HTTP POST with the X-HTTP-Method-Override header.
        if (request.Method == HttpMethod.Post && request.Headers.Contains(_header))
        {
            // Check if the header value is in our methods list.
            var method = request.Headers.GetValues(_header).FirstOrDefault();
            if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase))
            {
                // Change the request method.
                request.Method = new HttpMethod(method);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
}

SendAsync 메서드에서 처리기는 요청 메시지가 POST 요청인지 여부와 X-HTTP-Method-Override 헤더가 포함되어 있는지 여부를 확인합니다. 이 경우 헤더 값의 유효성을 검사한 다음 요청 메서드를 수정합니다. 마지막으로 처리기는 를 호출 base.SendAsync 하여 메시지를 다음 처리기에 전달합니다.

요청이 HttpControllerDispatcher 클래스에 도달하면 HttpControllerDispatcher 는 업데이트된 요청 메서드를 기반으로 요청을 라우팅합니다.

예: 사용자 지정 응답 헤더 추가

다음은 모든 응답 메시지에 사용자 지정 헤더를 추가하는 메시지 처리기입니다.

// .Net 4.5
public class CustomHeaderHandler : DelegatingHandler
{
    async protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
        response.Headers.Add("X-Custom-Header", "This is my custom header.");
        return response;
    }
}

먼저 처리기는 를 호출 base.SendAsync 하여 내부 메시지 처리기에 요청을 전달합니다. 내부 처리기는 응답 메시지를 반환하지만 Task<T> 개체를 사용하여 비동기적으로 반환합니다. 응답 메시지는 비동기적으로 완료될 때까지 base.SendAsync 사용할 수 없습니다.

이 예제에서는 await 키워드(keyword) 사용하여 완료 후 SendAsync 비동기적으로 작업을 수행합니다. .NET Framework 4.0을 대상으로 하는 경우 작업<T>를 사용합니다. ContinueWith 메서드:

public class CustomHeaderHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(
            (task) =>
            {
                HttpResponseMessage response = task.Result;
                response.Headers.Add("X-Custom-Header", "This is my custom header.");
                return response;
            }
        );
    }
}

예: API 키 확인

일부 웹 서비스는 클라이언트가 요청에 API 키를 포함하도록 요구합니다. 다음 예제에서는 메시지 처리기가 유효한 API 키에 대한 요청을 검사 방법을 보여 줍니다.

public class ApiKeyHandler : DelegatingHandler
{
    public string Key { get; set; }

    public ApiKeyHandler(string key)
    {
        this.Key = key;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!ValidateKey(request))
        {
            var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            var tsc = new TaskCompletionSource<HttpResponseMessage>();
            tsc.SetResult(response);    
            return tsc.Task;
        }
        return base.SendAsync(request, cancellationToken);
    }

    private bool ValidateKey(HttpRequestMessage message)
    {
        var query = message.RequestUri.ParseQueryString();
        string key = query["key"];
        return (key == Key);
    }
}

이 처리기는 URI 쿼리 문자열에서 API 키를 찾습니다. (이 예제에서는 키가 정적 문자열이라고 가정합니다. 실제 구현은 더 복잡한 유효성 검사를 사용할 수 있습니다.) 쿼리 문자열에 키가 포함된 경우 처리기는 내부 처리기에 요청을 전달합니다.

요청에 유효한 키가 없는 경우 처리기는 상태 403, 사용할 수 없음을 사용하여 응답 메시지를 만듭니다. 이 경우 처리기는 를 호출 base.SendAsync하지 않으므로 내부 처리기는 요청을 수신하지 않으며 컨트롤러도 수신하지 않습니다. 따라서 컨트롤러는 들어오는 모든 요청에 유효한 API 키가 있다고 가정할 수 있습니다.

참고

API 키가 특정 컨트롤러 작업에만 적용되는 경우 메시지 처리기 대신 작업 필터를 사용하는 것이 좋습니다. 작업 필터는 URI 라우팅이 수행된 후 실행됩니다.

Per-Route 메시지 처리기

HttpConfiguration.MessageHandlers 컬렉션의 처리기는 전역적으로 적용됩니다.

또는 경로를 정의할 때 특정 경로에 메시지 처리기를 추가할 수 있습니다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "Route1",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "Route2",
            routeTemplate: "api2/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: null,
            handler: new MessageHandler2()  // per-route message handler
        );

        config.MessageHandlers.Add(new MessageHandler1());  // global message handler
    }
}

이 예제에서 요청 URI가 "Route2"에 일치하면 요청이 로 MessageHandler2디스패치됩니다. 다음 다이어그램은 이러한 두 경로에 대한 파이프라인을 보여 줍니다.

경로를 정의하여 특정 경로에 메시지 처리기를 추가하는 프로세스를 보여 주는 경로별 메시지 처리기 파이프라인 다이어그램

MessageHandler2 기본 HttpControllerDispatcher를 대체합니다. 이 예제 MessageHandler2 에서는 응답을 만들고 "Route2"와 일치하는 요청은 컨트롤러로 이동하지 않습니다. 이렇게 하면 전체 Web API 컨트롤러 메커니즘을 사용자 고유의 사용자 지정 엔드포인트로 바꿀 수 있습니다.

또는 경로별 메시지 처리기가 HttpControllerDispatcher에 위임한 다음 컨트롤러에 디스패치할 수 있습니다.

h t t p 컨트롤러 디스패처에 위임하는 프로세스를 보여 주는 경로별 메시지 처리기 파이프라인의 다이어그램. 그러면 컨트롤러에 디스패치됩니다.

다음 코드는 이 경로를 구성하는 방법을 보여줍니다.

// List of delegating handlers.
DelegatingHandler[] handlers = new DelegatingHandler[] {
    new MessageHandler3()
};

// Create a message handler chain with an end-point.
var routeHandlers = HttpClientFactory.CreatePipeline(
    new HttpControllerDispatcher(config), handlers);

config.Routes.MapHttpRoute(
    name: "Route2",
    routeTemplate: "api2/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: null,
    handler: routeHandlers
);