namalzi

← 글 목록

Apache httpd 소스 파헤치기 10: 요청 처리 단계 — URL에서 파일과 핸들러까지

Apache httpd · 게시

GET /hello.html 한 줄이 httpd 안에서 어떤 단계를 거쳐 디스크의 파일과 그것을 내보낼 핸들러로 바뀌는지, ap_process_request_internal()의 단계 순서를 따라 소스에서 확인합니다. Location·Directory·Files 설정이 합쳐지는 세 가지 걷기, mod_dir의 서브요청, ErrorDocument의 내부 리다이렉트도 모든 단계에 로그를 찍는 모듈로 직접 봅니다.

이 글에서 다루는 것

"Apache httpd 소스 파헤치기" 연재의 열 번째 글입니다. 9편에서는 소켓에서 요청 줄과 헤더를 읽어 request_rec 하나를 만들고, Host 헤더로 가상 호스트를 고르는 데까지 봤습니다. 이번에는 그 request_rec를 넘겨받아 "이 URL은 디스크의 어느 파일이고, 어떤 설정이 적용되며, 누가 응답을 만들 것인가" 를 정하는 과정을 따라갑니다. 연재 전체에서 가장 중요한 길목입니다.

답하려는 질문은 네 가지입니다.

항목 내용
대상 Apache httpd
버전 2.5.1-dev (trunk)
커밋 e4af30fe6d (2026-10-24)
주로 읽는 파일 modules/http/http_request.c, server/request.c, server/config.c, server/core.c, modules/mappers/mod_dir.c

글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.

server/request.c는 2,600줄이 넘는 파일이고, 그중 디렉터리 걷기 함수 하나만 800줄입니다. 이 글은 처음 보는 사람이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "이 글에서 뺀 것"에 이름과 위치만 적어 두었습니다.

먼저 알아 둘 말

훅(hook)은 "이 시점이 되면 등록된 함수들을 차례로 불러 주겠다"고 코어가 열어 둔 자리입니다. 모듈은 ap_hook_translate_name(내_함수, ...)처럼 자기 함수를 등록해 두고, 코어는 그 시점에 ap_run_translate_name(r)을 불러 등록된 함수들을 돌립니다. 함수를 값처럼 넘기는 함수 포인터는 2편에서, 훅을 만드는 매크로와 순서 정하기는 6편에서 봤습니다.

RUN_FIRST와 RUN_ALL은 훅을 돌리는 두 가지 방식입니다. 훅 함수는 보통 OK(처리했다), DECLINED(내 일이 아니다), 또는 HTTP 오류 코드(예: 404) 가운데 하나를 돌려줍니다.

request_rec 는 요청 하나에 대한 모든 것을 담는 구조체입니다(3편). 이 글에서는 필드 네 개만 따라다닙니다.

필드 뜻 예
r->uri 요청한 URL의 경로 부분 /docs/index.html
r->filename 그 URL이 가리키는 디스크 경로 /tmp/htdocs/docs/index.html
r->handler 응답을 만들 핸들러의 이름 text/html, server-status
r->per_dir_config 이 요청에 적용할, 합쳐진 디렉터리 설정 (모듈별 설정 구조체들의 배열)

디렉터리 설정(per-dir config)은 <Directory>, <Location>, <Files>, .htaccess 안에 적는 설정입니다. 설정 파일을 읽을 때 섹션마다 따로 만들어 두었다가 요청이 오면 그 요청에 맞는 섹션들을 골라 병합(merge)합니다. 섹션 하나가 C 구조체가 되는 과정과 병합 함수는 7편에서 봤습니다. 이 글은 "요청이 올 때 어떤 섹션을 어떤 순서로 고르나"를 봅니다.

서브요청(subrequest)은 요청을 처리하는 도중에 "이 URL을 요청했다면 어떻게 됐을까"를 미리 알아보려고 만드는 작은 요청입니다. 클라이언트는 모릅니다. 내부 리다이렉트(internal redirect)는 응답을 아예 다른 URL로 바꿔 처음부터 다시 처리하는 것입니다. 브라우저 주소창이 바뀌는 301/302 리다이렉트와 달리, 이것도 서버 안에서만 일어납니다.

1. 큰 그림: 세 함수와 마무리 하나

9편에서 본 연결 처리 루프는 요청을 하나 읽을 때마다 ap_process_async_request()(event MPM처럼 비동기를 쓰는 경우) 또는 ap_process_request()를 부릅니다(modules/http/http_core.c 159행, 212행). 뒤쪽도 안에서 앞쪽을 부르므로(486행) 본론은 ap_process_async_request()에 있습니다. 로그와 통계를 빼면 뼈대는 이렇습니다.

    access_status = ap_run_quick_handler(r, 0);  /* Not a look-up request */
    if (access_status == DECLINED) {
        access_status = ap_process_request_internal(r);
        if (access_status == OK) {
            access_status = ap_invoke_handler(r);
        }
    }
    /* ... */
    ap_die_r(access_status, r, HTTP_OK);

    ap_process_request_after_handler(r);
}

modules/http/http_request.c 447–477행

요청 처리의 큰 흐름. ap_read_request가 request_rec를 만들면 quick_handler 훅이 먼저 불린다. OK면 바로 ap_die_r로 가고, DECLINED면 ap_process_request_internal이 단계들을 돌린다. 그 결과가 OK면 ap_invoke_handler가 insert_filter 훅과 handler 훅을 부르고, 오류면 바로 ap_die_r로 간다. ap_die_r는 OK면 응답을 마무리하고 오류면 오류 응답을 보내는데, ErrorDocument가 로컬 주소면 ap_internal_redirect로 같은 단계를 새 URI로 다시 돈다. 마지막으로 ap_process_request_after_handler가 EOR 버킷을 흘려 보내고, 그 버킷이 지워질 때 log_transaction 훅이 불리고 요청 풀이 해제된다. 파란 상자가 이 글의 주인공입니다. 빨간 상자는 오류가 났을 때만 가는 길입니다.

  1. quick_handler 훅 — 아무 단계도 거치기 전에 "이 요청을 통째로 맡을 사람?" 하고 묻습니다. RUN_FIRST입니다(server/config.c 172–173행). URL을 열쇠로 하는 캐시가 쓰라고 만든 자리이고, 실제로 mod_cache가 여기에 걸립니다(modules/cache/mod_cache.c 2605행). 여기서 OK가 나오면 아래 단계를 전부 건너뜁니다. 접근 제어도 건너뛰므로, 소스 주석도 "아주 조심해서 쓰라"고 적어 두었습니다(422–423행).
  2. ap_process_request_internal() — URL을 파일로 바꾸고, 설정을 합치고, 접근을 검사하고, 내용 형식을 정합니다. 2절부터 볼 이 글의 중심입니다.
  3. ap_invoke_handler() — 응답 본문을 만들 핸들러를 불러냅니다. 5절에서 봅니다.
  4. ap_die_r() — 이름과 달리 정상일 때도 지나갑니다. OK나 DONE이면 ap_finalize_request_protocol()로 응답을 마무리하고 끝납니다(82–85행). 오류 코드면 오류 응답을 보내는데, 그 길에 ErrorDocument가 있습니다(6절).

마지막의 ap_process_request_after_handler()는 연결의 출력 필터로 EOR(End Of Request) 버킷을 흘려 보냅니다(336–349행). 이 버킷이 지워질 때 접근 로그를 남기는 log_transaction 훅이 불리고(server/eor_bucket.c 27–46행) 요청 풀이 지워집니다. 버킷과 필터는 11편에서, 로그는 16편에서 봅니다.

예제 1: 핸들러에 닿았을 때의 호출 스택

정말 이 길로 가는지 gdb로 확인했습니다. 정적 파일 하나를 요청하고, 그 파일을 내보내는 코어의 default_handler()에 중단점을 걸어 호출 스택을 찍었습니다.

이 예제는 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev 로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. ex/u10-handler-stack/run.sh의 핵심은 이렇습니다.

# -X: 자식 프로세스를 따로 띄우지 않고 한 프로세스에서 요청을 받는다 (디버깅용)
( sleep 2; curl -s http://127.0.0.1:8080/hello.html >/dev/null ) &
gdb -batch -q \
    -ex "set breakpoint pending on" \
    -ex "break default_handler" \
    -ex "run" \
    -ex "bt" \
    -ex "kill" \
    --args httpd -X -f /tmp/u10s.conf 2>&1 | grep -E "^#|Breakpoint 1,"
Thread 3 "httpd" hit Breakpoint 1, 0x000055beb9cfe8da in default_handler ()
#0  0x000055beb9cfe8da in default_handler ()
#1  0x000055beb9cc484d in ap_run_handler ()
#2  0x000055beb9cc53f9 in ap_invoke_handler ()
#3  0x000055beb9cb6e81 in ap_process_async_request ()
#4  0x000055beb9cafdd5 in ap_process_http_async_connection ()
#5  0x000055beb9cb0003 in ap_process_http_connection ()
#6  0x000055beb9cddc11 in ap_run_process_connection ()
#7  0x00007f05972698b2 in process_socket () from /opt/httpd/modules/mod_mpm_event.so
#8  0x00007f059726dbc7 in worker_thread () from /opt/httpd/modules/mod_mpm_event.so
#9  0x000055beb9cdaff9 in thread_start ()
#10 0x00007f05973c7b84 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#11 0x00007f0597454cc4 in clone () from /lib/x86_64-linux-gnu/libc.so.6

주소 값은 실행마다 다릅니다. 이 빌드에는 디버그 정보가 없어 파일·줄 번호 없이 함수 이름만 나옵니다. 아래에서 위로 읽으면 event MPM의 작업 스레드(worker_thread, 8편) → 연결 처리(9편) → ap_process_async_request() → ap_invoke_handler() → 핸들러 훅입니다.

헷갈리기 쉬운 곳: 스택에 ap_process_request_internal()이 없습니다

단계들을 다 도는 함수인데 스택에 없습니다. 이 함수는 핸들러를 부르지 않고 OK를 돌려주고 끝나기 때문입니다. 핸들러를 부르는 것은 그다음 줄의 ap_invoke_handler()입니다. "준비하는 함수"와 "실행하는 함수"가 나뉘어 있어서, 뒤에서 볼 서브요청은 준비만 하고 실행은 하지 않을 수 있습니다.

2. ap_process_request_internal()의 단계 순서

함수 머리의 주석이 이 함수의 무게를 말해 줍니다.

This is the master logic for processing requests. Do NOT duplicate this logic elsewhere, or the security model will be broken by future API changes.

(server/request.c 187–191행) 본 요청이든 서브요청이든 내부 리다이렉트든, URL을 해석하는 일은 전부 이 함수 하나를 거칩니다. 그래야 접근 제어를 빠뜨리는 길이 생기지 않습니다.

ap_process_request_internal의 단계. 경로 정규화 다음에 location walk와 pre_translate_name 훅, 다시 location walk와 translate_name 훅으로 URI를 r->filename으로 바꾼다. per_dir_config를 서버 기본값으로 되돌리고 map_to_storage 훅에서 directory walk와 file walk를 한다. location walk를 다시 해서 Location이 Directory를 덮어쓰게 하고, 본 요청에서만 header_parser 훅을 부른다. 그다음 인증과 권한 단계, type_checker 훅, fixups 훅을 거쳐 OK를 돌려준다. 파란 상자 하나하나가 모듈이 끼어들 수 있는 자리입니다.

함수 가운데 토막을 그대로 옮기면 이렇습니다. 위의 그림 3~6번째 상자에 해당합니다.

    /* Same, translate_name is not suited for file subrequests */
    if (!file_req) {
        if ((access_status = walk_location_and_if(r))) {
            return access_status;
        }

        if ((access_status = ap_run_translate_name(r))) {
            return decl_die(access_status, "translate", r);
        }
    }

    /* Reset to the server default config prior to running map_to_storage
     */
    r->per_dir_config = r->server->lookup_defaults;

    if ((access_status = ap_run_map_to_storage(r))) {
        /* This request wasn't in storage (e.g. TRACE) */
        return access_status;
    }

    /* Rerun the location walk, which overrides any map_to_storage config.
     */
    if ((access_status = walk_location_and_if(r))) {
        return access_status;
    }

    if ((access_status = ap_run_post_perdir_config(r))) {
        return access_status;
    }

    /* Only on the main request! */
    if (r->main == NULL) {
        if ((access_status = ap_run_header_parser(r))) {
            return access_status;
        }
    }

server/request.c 277–312행

모양이 전부 같습니다. 훅을 돌리고, 0(OK)이 아닌 값이 나오면 그 값을 들고 바로 돌아갑니다. 돌려받은 ap_process_async_request()는 그 값을 ap_die_r()에 넘겨 오류 응답을 만듭니다. 어느 단계든 403이나 404를 돌려주면 요청은 거기서 끝난다는 뜻입니다.

전체 단계를 표로 정리합니다. "기본 담당"은 아무 모듈도 맡지 않을 때 코어가 걸어 둔 함수입니다.

순서 훅 방식 하는 일 기본 담당 / 대표 모듈
0 post_read_request RUN_ALL 요청을 다 읽은 직후 (9편의 끝) mod_setenvif, mod_proxy
1 quick_handler RUN_FIRST 지름길 mod_cache
2 pre_translate_name RUN_FIRST %xx를 풀기 전의 URI를 볼 기회 mod_proxy
3 translate_name RUN_FIRST URI → r->filename 코어 ap_core_translate(), mod_alias, mod_rewrite
4 map_to_storage RUN_FIRST 파일에 맞는 설정 고르기 코어 core_map_to_storage()
5 header_parser RUN_ALL 헤더를 보고 할 일 (본 요청만) mod_setenvif 등
6 access_checker 외 여러 가지 접근·인증·권한 mod_authz_core 등 (13편)
7 type_checker RUN_FIRST Content-Type, 핸들러 이름 정하기 mod_mime
8 fixups RUN_ALL 핸들러 직전의 마지막 손질 mod_dir, mod_alias, mod_rewrite

훅마다의 방식은 소스에서 직접 확인했습니다(server/request.c 79–108행, server/config.c 85–86행, server/protocol.c 2554–2557행). 코어가 맨 끝(APR_HOOK_REALLY_LAST)에 걸어 두는 함수는 server/core.c 6164–6173행에 있습니다.

왜 RUN_FIRST 훅마다 코어가 맨 끝에 서 있나

translate_name의 결과는 decl_die()를 거칩니다. 이 함수는 모두가 DECLINED했으면 "설정 오류"로 보고 500을 돌려줍니다(115–128행). URL을 파일로 바꾸는 사람이 아무도 없으면 진행할 수 없기 때문입니다. 그래서 코어는 누군가 꼭 답해야 하는 RUN_FIRST 단계마다 맨 마지막 순서로 무조건 답하는 함수를 걸어 둡니다. translate_name에는 ap_core_translate(), map_to_storage에는 core_map_to_storage(), type_checker에는 아무것도 하지 않고 OK만 돌려주는 do_nothing(), 핸들러에는 default_handler()가 그것입니다. 소스 주석도 이 do_nothing들을 언젠가 없애고 싶다고 적어 두었습니다(6172행).

예제 2: 모든 단계에 로그를 찍는 모듈

단계들을 눈으로 보려고, 모든 훅에 같은 함수를 걸어 그 순간의 r->uri, r->filename, r->handler를 찍는 모듈을 만들었습니다. 모든 함수가 DECLINED를 돌려주므로 서버의 동작은 바뀌지 않습니다.

/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
/* 요청 처리의 모든 단계에 훅을 걸고, 단계마다 r->uri, r->filename, r->handler 를 찍는다. */
/* ... #include 생략 ... */

/* 이 요청이 본 요청인지, 서브요청(r->main)인지, 내부 리다이렉트(r->prev)인지 */
static const char *kind(request_rec *r)
{
    if (r->main) return "sub     ";
    if (r->prev) return "redirect";
    return "main    ";
}

static void trace(request_rec *r, const char *phase)
{
    ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r,
                  "%s %-18s uri=%s filename=%s handler=%s",
                  kind(r), phase, r->uri,
                  r->filename ? r->filename : "(null)",
                  r->handler ? r->handler : "(null)");
}

/* 단계마다 같은 일을 하는 함수를 매크로로 찍어 낸다. 모두 DECLINED 를 돌려준다 —
 * "나는 상관없으니 다음 모듈에게 넘겨라" 라는 뜻이라 서버의 동작은 바뀌지 않는다. */
#define PHASE(name) \
    static int journey_##name(request_rec *r) { trace(r, #name); return DECLINED; }

PHASE(post_read_request)
PHASE(pre_translate_name)
PHASE(translate_name)
/* ... map_to_storage 부터 handler 까지 같은 모양 ... */

static void register_hooks(apr_pool_t *p)
{
    /* 다른 모듈보다 먼저 불리도록 REALLY_FIRST — 그래서 각 줄은 "그 단계에 들어설 때"의 값이다 */
    ap_hook_post_read_request(journey_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_quick_handler(journey_quick_handler, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_pre_translate_name(journey_pre_translate_name, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_translate_name(journey_translate_name, NULL, NULL, APR_HOOK_REALLY_FIRST);
    /* ... 나머지 훅도 같은 방식으로 등록 ... */
}

[예제 ex/u10-request-phases/mod_journey.c에서 줄임]

#name은 매크로 인자를 문자열로 바꾸고 journey_##name은 이름을 이어 붙이는 C 전처리기 문법입니다(2편). 설정은 DocumentRoot /tmp/htdocs, <Directory /tmp/htdocs> 안에 Require all granted, 그리고 ErrorLogFormat "%M"으로 오류 로그에 메시지만 남게 했습니다. /hello.html을 요청한 결과입니다.

===== curl http://127.0.0.1:8080/hello.html =====
HTTP 200  body: hello
main     post_read_request  uri=/hello.html filename=(null) handler=(null)
main     quick_handler      uri=/hello.html filename=(null) handler=(null)
main     pre_translate_name uri=/hello.html filename=(null) handler=(null)
main     translate_name     uri=/hello.html filename=(null) handler=(null)
main     map_to_storage     uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     header_parser      uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     access_checker     uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     type_checker       uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     fixups             uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     insert_filter      uri=/hello.html filename=/tmp/htdocs/hello.html handler=(null)
main     handler            uri=/hello.html filename=/tmp/htdocs/hello.html handler=text/html
main     log_transaction    uri=/hello.html status=200

읽을 것이 세 가지 있습니다.

인증·권한 단계는 순서만

6번 단계는 사실 훅 여러 개입니다. Satisfy All(기본)일 때의 순서는 이렇습니다(339–368행).

  1. token_checker — 토큰(예: Bearer) 인증. trunk에만 있는 훅으로, 2.4.x 브랜치의 server/request.c에는 이 훅이 없습니다.
  2. access_checker — IP 같은, 사용자와 상관없는 검사. RUN_ALL.
  3. access_checker_ex — RUN_FIRST. mod_authz_core가 여기서 Require를 판정합니다. 사용자 없이 허가되면 OK, 사용자가 필요하면 DECLINED입니다.
  4. 3이 DECLINED였을 때만(또는 OK였어도 force_authn 훅이 인증을 요구하면) check_user_id(누구인가, 인증)와 auth_checker(그 사람이 되는가, 권한)를 차례로 부릅니다.

예제 2에서 두 단계가 빠진 것은 3번에서 Require all granted가 OK를 냈기 때문입니다. 암호가 걸린 디렉터리(AuthType Basic, Require valid-user)로 요청하면 두 단계가 나타납니다.

===== curl -u alice:wonderland http://127.0.0.1:8080/private/secret.txt =====
HTTP 200  body: secret
main     post_read_request  uri=/private/secret.txt filename=(null) handler=(null)
main     quick_handler      uri=/private/secret.txt filename=(null) handler=(null)
main     pre_translate_name uri=/private/secret.txt filename=(null) handler=(null)
main     translate_name     uri=/private/secret.txt filename=(null) handler=(null)
main     map_to_storage     uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     header_parser      uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     access_checker     uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     check_user_id      uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     auth_checker       uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     type_checker       uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     fixups             uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     insert_filter      uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=(null)
main     handler            uri=/private/secret.txt filename=/tmp/htdocs/private/secret.txt handler=text/plain
main     log_transaction    uri=/private/secret.txt status=200

Require 한 줄이 어떻게 판정되는지, Satisfy Any일 때 순서가 어떻게 바뀌는지는 13편에서 봅니다.

헷갈리기 쉬운 곳

3. URL을 파일로: translate_name과 map_to_storage

ap_core_translate() — DocumentRoot에 URI를 붙인다

translate_name의 마지막 담당인 코어 함수의 핵심은 한 호출입니다.

    if ((rv = apr_filepath_merge(&r->filename, ap_document_root(r), path,
                                 APR_FILEPATH_TRUENAME
 APR_FILEPATH_SECUREROOT, r->pool))
                != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(00127)
                     "Cannot map %s to file", r->the_request);
        return HTTP_FORBIDDEN;
    }
    r->canonical_filename = r->filename;

    return OK;

server/core.c 5079–5089행

DocumentRoot(/tmp/htdocs)와 앞의 /를 떼어 낸 URI(hello.html)를 APR의 경로 합치기 함수로 붙입니다. APR_FILEPATH_SECUREROOT는 결과가 DocumentRoot 밖으로 나가면 실패하게 하는 플래그입니다. 그 앞에서 ap_process_request_internal()이 이미 /../를 정리했지만(209–221행), 여기서 한 번 더 막습니다.

mod_alias와 mod_rewrite가 끼는 자리

Alias /icons/ /usr/share/icons/ 같은 설정은 이 단계에서 코어보다 먼저 답해 버리는 방식으로 동작합니다.

    ap_hook_translate_name(translate_alias_redir,NULL,aszSucc,APR_HOOK_MIDDLE);
    ap_hook_fixups(fixup_redir,NULL,NULL,APR_HOOK_MIDDLE);

modules/mappers/mod_alias.c 788–789행

mod_alias는 MIDDLE, 코어는 REALLY_LAST이므로 mod_alias가 먼저 불립니다. Alias에 맞으면 r->filename을 정하고 OK를 돌려줍니다(690–695행). RUN_FIRST이므로 그 순간 코어의 ap_core_translate()는 불리지 않습니다. Redirect에 맞으면 Location 헤더를 넣고 302 같은 상태 코드를 돌려주는데, 2절에서 본 대로 요청은 그 자리에서 끝나 브라우저에 리다이렉트 응답이 갑니다.

mod_rewrite는 APR_HOOK_FIRST로, 그리고 "mod_proxy.c보다 먼저"라는 조건을 붙여 겁니다(mod_rewrite.c 5765–5769행). 서버 설정의 RewriteRule은 이 translate_name에서 돌고, .htaccess나 <Directory> 안의 RewriteRule은 fixups에서 돕니다. 디렉터리 설정은 map_to_storage가 끝나야 알 수 있기 때문입니다. fixups에서 URL을 바꾼 경우에는 이미 단계를 다 지나왔으므로, r->filename 앞에 redirect:를 붙이고 핸들러를 자기 것으로 바꿔 두었다가(5654–5655행), 핸들러 단계에서 6절의 내부 리다이렉트로 처음부터 다시 돌립니다(5700–5717행). mod_rewrite의 규칙 처리 자체는 이 글에서 다루지 않습니다.

core_map_to_storage() — 걷기 두 번

map_to_storage 단계의 코어 담당은 짧습니다.

static int core_map_to_storage(request_rec *r)
{
    int access_status;

    if ((access_status = ap_directory_walk(r))) {
        return access_status;
    }

    if ((access_status = ap_file_walk(r))) {
        return access_status;
    }

    return OK;
}

server/core.c 5096–5109행

이름 그대로 "저장소(디스크)에 맞춰 보는" 단계입니다. 디스크 파일이 아닌 것을 다루는 모듈은 여기서 먼저 답해 디렉터리 걷기를 건너뜁니다. mod_proxy가 APR_HOOK_FIRST로 여기에 거는 것이 그 예입니다(mod_proxy.c 3437행, 14편).

GET /hello.html 의 r->uri 가 r->filename 이 되기까지. translate_name 훅은 RUN_FIRST 라서 먼저 OK 를 낸 쪽이 이긴다. mod_proxy.c 보다 먼저로 FIRST 에 건 mod_rewrite 가 서버 설정의 RewriteRule 을 보고, DECLINED 면 MIDDLE 의 mod_alias 가 Alias 에 맞으면 r->filename 을 정하고 OK 를, Redirect 에 맞으면 302 등으로 요청을 끝낸다. 그것도 DECLINED 면 REALLY_LAST 의 ap_core_translate 가 DocumentRoot /tmp/htdocs 에 hello.html 을 apr_filepath_merge(SECUREROOT)로 붙여 r->filename 이 /tmp/htdocs/hello.html 이 된다. 이어지는 map_to_storage 도 RUN_FIRST 라서 FIRST 의 mod_proxy 가 디스크가 아닌 것은 먼저 답해 걷기를 건너뛰고, DECLINED 면 core_map_to_storage 가 ap_directory_walk() 와 ap_file_walk() 를 부른다 파란 칸은 코어보다 먼저 답할 수 있는 모듈, 노란 칸은 맨 끝에 선 코어입니다. 경로는 예제 2의 map_to_storage 줄에서 가져왔습니다.

4. 세 가지 걷기 — 설정이 합쳐지는 곳

"걷기"(walk)는 설정 파일의 섹션 목록을 처음부터 훑으며 이 요청에 맞는 것을 골라 차례로 병합하는 일입니다. 세 가지가 있고 각각 다른 목록을, 다른 기준으로 봅니다.

함수 보는 목록 무엇과 비교하나 위치
ap_directory_walk() <Directory>, <DirectoryMatch>, .htaccess r->filename의 디렉터리 경로 680–1484행
ap_file_walk() <Files>, <FilesMatch> r->filename의 마지막 이름 1690행부터
ap_location_walk() <Location>, <LocationMatch> r->uri 1487–1688행

설정 파일을 읽을 때 <Directory> 섹션은 서버 설정의 sec_dir 배열에, <Location>은 sec_url 배열에 하나씩 쌓입니다(server/core.c 639–653행). <Files>는 그것을 감싼 디렉터리 설정의 sec_file에 들어갑니다(655–665행). 걷기 함수들은 이 배열을 훑습니다.

설정이 합쳐지는 순서. 어느 섹션에도 없는 서버 전체 설정인 lookup_defaults에서 시작해 directory walk가 Directory를 짧은 경로부터, 허용됐으면 디렉터리 한 단계마다 .htaccess를 합친다. directory walk 끝에 정규식 DirectoryMatch를 합치고, file walk가 Files와 FilesMatch를 파일 이름으로, location walk가 Location과 LocationMatch를 URL로, if walk가 If를 합쳐 이 요청에 쓸 r->per_dir_config가 된다. 노란 상자가 출발점과 결과, 파란 상자가 차례로 덮어쓰는 걷기입니다. 아래로 갈수록 나중에 합쳐지므로 우선합니다.

directory walk — 디렉터리를 한 단계씩 내려간다

ap_directory_walk()는 /tmp/htdocs/docs/index.html이라면 /, /tmp/, /tmp/htdocs/, /tmp/htdocs/docs/ 순으로 한 단계씩 내려가며, 각 단계에서 두 가지를 합니다.

  1. 그 깊이에 맞는 <Directory> 섹션을 찾아 합칩니다(1018–1084행). 섹션 목록은 설정을 다 읽은 뒤 경로의 마디 수가 적은 것부터, 정규식은 맨 뒤로 정렬해 두었습니다(server/core.c 709–741행). 그래서 설정 파일에 적은 순서와 상관없이 <Directory />가 <Directory /tmp/htdocs>보다 먼저 합쳐집니다.
  2. AllowOverride로 허용되어 있으면 그 디렉터리의 .htaccess를 읽어 합칩니다(1086–1110행).

정규식 섹션(<DirectoryMatch>, <Directory ~ ...>)은 내려가기가 끝난 뒤 따로 한 번 훑어 합칩니다(1332–1409행). 걷는 동안 디스크에 stat을 해 보면서, 실제 파일 뒤에 남은 부분은 r->path_info로 떼어 냅니다(/script.cgi/extra의 /extra).

location walk — 그리고 마지막에 병합

ap_location_walk()는 디스크를 보지 않고 r->uri 문자열을 섹션의 경로와 비교합니다. 세 걷기 모두 끝부분이 같습니다. 고른 섹션들을 차례로 합친 now_merged를 만든 뒤, 그것을 지금까지의 r->per_dir_config 위에 한 번 더 병합합니다.

    cache->dir_conf_tested = sec_ent;
    cache->dir_conf_merged = r->per_dir_config;

    /* Merge our cache->dir_conf_merged construct with the r->per_dir_configs,
     * and note the end result to (potentially) skip this step next time.
     */
    if (now_merged) {
        r->per_dir_config = ap_merge_per_dir_configs(r->pool,
                                                     r->per_dir_config,
                                                     now_merged);
    }
    cache->per_dir_result = r->per_dir_config;

    return OK;
}

server/request.c 1674–1688행

ap_merge_per_dir_configs()가 7편에서 본 그 병합 함수입니다. 모듈마다 등록한 merge_dir_config 함수를 불러 "기존 설정 + 새 설정"을 만듭니다(server/config.c 285행). 2절에서 본 순서 — map_to_storage(디렉터리·파일 걷기) 다음에 location walk — 때문에 <Location>이 <Directory>를 덮어씁니다. 299행의 주석 "Rerun the location walk, which overrides any map_to_storage config"가 바로 이 말입니다. <If> 섹션은 location walk 바로 뒤에 ap_if_walk()가 합칩니다(walk_location_and_if() 166–190행).

cache로 시작하는 줄들은 같은 요청 안에서, 또는 서브요청·내부 리다이렉트가 부모와 같은 섹션을 고르게 될 때 병합을 다시 하지 않으려는 캐시입니다. walk_cache_t라는 구조체로 요청마다 보관합니다(455–468행). 이 글에서는 "있다"는 것만 알아 두고, 6절에서 그 효과 하나를 봅니다.

헷갈리기 쉬운 곳: 파일이 없어도 여기서는 404가 나지 않습니다

ap_directory_walk()는 내려가다 파일이 없으면 걷기를 멈출 뿐 오류를 돌려주지 않습니다(APR_ENOENT일 때 break, 1228–1234행). 없는 파일을 요청해도 단계는 끝까지 갑니다. 404를 정하는 것은 마지막의 default_handler()입니다.

    if (r->method_number == M_GET || r->method_number == M_POST) {
        if (r->finfo.filetype == APR_NOFILE) {
            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(00128)
                          "File does not exist: %s",
                          apr_pstrcat(r->pool, r->filename, r->path_info, NULL));
            return HTTP_NOT_FOUND;
        }

server/core.c 5192–5198행

이렇게 늦게 정하는 데는 이유가 있습니다. 파일이 없더라도 다른 모듈(예: 핸들러가 정해진 <Location>, 6절의 mod_dir)이 그 URL을 맡을 수 있기 때문입니다. 6절의 예제 4에서 AH00128 로그가 handler 단계 다음에 찍히는 것으로 확인합니다.

5. ap_invoke_handler() — 핸들러를 고르고 부른다

ap_process_request_internal()이 OK를 돌려주면 ap_invoke_handler()가 불립니다. 핵심만 옮깁니다.

AP_CORE_DECLARE(int) ap_invoke_handler(request_rec *r)
{
    /* ... */
    ap_run_insert_filter(r);
    /* ... 필터의 초기화 함수 부르기 ... */
    if (!r->handler) {
        if (r->content_type && AP_REQUEST_IS_TRUSTED_CT(r)) {
            handler = r->content_type;
            /* ... "text/html; charset=..." 의 ; 뒤를 잘라 낸다 ... */
        }
        else {
            handler = AP_DEFAULT_HANDLER_NAME;
        }

        r->handler = handler;
    }

    result = ap_run_handler(r);

    r->handler = old_handler;
    /* ... */
    return result == DECLINED ? HTTP_INTERNAL_SERVER_ERROR : result;
}

server/config.c 389–470행

  1. insert_filter 훅 — 이제 진짜 응답을 만들 것이므로, 모듈들이 출력 필터를 끼워 넣을 마지막 기회입니다(405행). 반환값이 없는 VOID 훅입니다(server/request.c 97행).
  2. 핸들러 이름 정하기 — SetHandler나 AddHandler로 r->handler가 정해져 있으면 그대로 씁니다. 없으면 Content-Type을 핸들러 이름으로 씁니다(420–441행). 예제 2에서 handler=text/html이 찍힌 이유가 이것입니다. mod_mime이 type_checker 단계에서 .html을 보고 Content-Type을 text/html로 정했고(믿을 수 있는 값이라는 표시와 함께, mod_mime.c 921행), 그것이 핸들러 이름이 되었습니다. 그것도 없으면 빈 문자열(AP_DEFAULT_HANDLER_NAME, include/httpd.h 731행)입니다.
  3. handler 훅 — RUN_FIRST입니다(server/config.c 169–170행). 핸들러 모듈들은 대개 맨 앞에서 r->handler를 자기 이름과 비교해 다르면 DECLINED를 돌려줍니다. mod_status는 "server-status"(mod_status.c 217–219행), mod_autoindex는 디렉터리 표시용 이름을 봅니다. 아무도 맡지 않은 요청은 맨 끝의 default_handler()가 받아 파일을 그대로 보냅니다. 핸들러가 text/html이라는 이름을 맡겠다고 나서는 모듈은 없으므로, 예제의 /hello.html은 default_handler()가 보냈습니다. 예제 1의 스택이 그 증거입니다.
  4. 끝까지 DECLINED면 500 (469행).

ap_invoke_handler() 가 핸들러를 고르는 순서. ① insert_filter 훅으로 출력 필터를 끼울 마지막 기회를 준다. r->handler 가 SetHandler 나 AddHandler 로 정해져 있으면 그대로 쓴다. 없으면 믿을 수 있는 Content-Type 이 있을 때 그것을 핸들러 이름으로 쓰고(/hello.html 이면 mod_mime 이 정한 text/html), 그것도 없으면 빈 문자열 AP_DEFAULT_HANDLER_NAME 을 쓴다. ② ap_run_handler() 는 RUN_FIRST 라서 모듈마다 r->handler 를 자기 이름과 비교하고(mod_status 는 server-status 만 맡는다), 아무도 맡지 않으면 맨 끝의 default_handler() 가 파일을 그대로 보낸다. 끝까지 DECLINED 면 500 이다. ③ 끝나면 r->handler 를 old_handler 로 되돌린다 위 목록의 네 항목을 갈림길로 그렸습니다. 노란 칸이 예제의 /hello.html 이 실제로 지난 길입니다.

default_handler()가 파일을 응답으로 바꾸는 과정, mod_mime이 확장자를 해석하는 방법은 12편에서 봅니다.

헷갈리기 쉬운 곳: 핸들러 이름이 끝나면 되돌아갑니다

445행의 r->handler = old_handler;는 이 함수 안에서 정한 기본 핸들러 이름을 핸들러가 끝난 뒤 지웁니다. 그래서 로그 모듈 같은 뒤쪽 코드에서 r->handler를 보면 NULL일 수 있습니다. 핸들러 이름이 Content-Type에서 왔다는 것은 ap_invoke_handler() 안에서만 유효한 사실입니다.

6. 서브요청과 내부 리다이렉트

두 기능 모두 "다른 URL에 대해 2절의 단계를 돌린다"는 점은 같지만, 목적과 결과가 다릅니다.

서브요청과 내부 리다이렉트의 비교. 왼쪽은 서브요청이다. fixups 단계의 mod_dir가 ap_sub_req_lookup_uri를 부르면 make_sub_request가 자식 풀과 r->main = 부모인 요청을 만들고, quick_handler lookup=1과 ap_process_request_internal을 돌린다. header_parser는 건너뛴다. 결과인 rr->status와 rr->finfo만 보고, 찾았으면 ap_internal_fast_redirect로 서브요청의 결과를 본 요청에 옮겨 담고, 못 찾았으면 ap_destroy_sub_req로 자식 풀을 지운다. 오른쪽은 내부 리다이렉트다. handler가 404를 돌려주면 ap_die_r의 ErrorDocument 처리가 internal_internal_redirect를 부르고, 같은 풀에 new->prev = r인 요청을 만들며 환경 변수에 REDIRECT_를 붙인다. 그리고 post_read_request부터 quick_handler, 처리 단계, handler까지 다시 돈다. 왼쪽은 "알아보기만" 하는 서브요청, 오른쪽은 "다시 처리하는" 내부 리다이렉트입니다.

서브요청 내부 리다이렉트
만드는 함수 ap_sub_req_lookup_uri() → make_sub_request() ap_internal_redirect() → internal_internal_redirect()
부모를 가리키는 필드 r->main r->prev (부모 쪽은 r->next)
풀 부모 풀의 자식 풀을 새로 만든다 부모와 같은 풀을 쓴다
단계 ap_process_request_internal()까지만. 실행은 따로 post_read_request부터 핸들러까지 전부
대표 사용처 mod_dir(DirectoryIndex), mod_autoindex, mod_include ErrorDocument, mod_rewrite의 디렉터리 규칙

서브요청: "이 URL이면 어떻게 되나" 알아보기

ap_sub_req_lookup_uri()는 메서드를 GET으로 정해 ap_sub_req_method_uri()를 부릅니다(2346–2351행). make_sub_request()가 부모 풀 아래에 "subrequest"라는 자식 풀을 만들어 그 안에 새 request_rec를 두고(2056–2059행), r->main에 부모를 기록합니다(server/protocol.c 1612행). 그다음이 본론입니다.

    if (next_filter) {
        res = ap_run_quick_handler(rnew, 1);
    }
    if (res == DECLINED) {
        res = ap_process_request_internal(rnew);
    }
    if (res) {
        rnew->status = res;
    }

    return rnew;
}

server/request.c 2333–2344행

1절의 본 요청과 같은 모양이지만 ap_invoke_handler()가 없습니다. 단계만 돌려 rnew->status, rnew->filename, rnew->finfo(파일 정보) 같은 결과를 담아 돌려줄 뿐입니다. 부르는 쪽은 그 결과를 보고 판단합니다. 실제로 응답을 만들게 하려면 따로 ap_run_sub_req()를 부르고(2577–2594행), 다 쓰면 ap_destroy_sub_req()로 자식 풀째 지웁니다. 풀을 지우면 그 안의 모든 것이 한 번에 사라진다는 것은 4편에서 봤습니다. quick_handler에 넘기는 둘째 인자 1은 "알아보는 중(lookup)"이라는 표시입니다.

DirectoryIndex가 이것으로 구현됩니다. mod_dir은 fixups 단계에서, 요청이 디렉터리를 가리키면 DirectoryIndex에 적힌 이름마다 서브요청을 만들어 봅니다.

        rr = ap_sub_req_lookup_uri(name_ptr, r, r->output_filters);
        /* ... */
        if (rr->status == HTTP_OK
            && (   (rr->handler && !strcmp(rr->handler, "proxy-server"))
| rr->finfo.filetype == APR_REG)) {

            if (ap_is_HTTP_REDIRECT(d->redirect_index)) {
                apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, rr->uri, r));
                return d->redirect_index;
            }

            ap_internal_fast_redirect(rr, r);
            return OK;
        }

modules/mappers/mod_dir.c 346–369행

서브요청이 200이고 그것이 보통 파일(APR_REG)이면, ap_internal_fast_redirect()로 서브요청의 결과를 본 요청에 옮겨 담습니다. r->uri, r->filename, r->finfo, r->handler, r->per_dir_config를 서브요청의 것으로 바꿔 끼우는 함수입니다(http_request.c 702–712행). 단계를 다시 돌지 않고 결과만 가져오므로 "fast"입니다.

예제 3: 디렉터리 요청과 mod_dir의 서브요청

/docs/를 요청했습니다. /tmp/htdocs/docs/index.html이 있고, 설정에 DirectoryIndex index.html이 있습니다.

===== curl http://127.0.0.1:8080/docs/ =====
HTTP 200  body: docs index
main     post_read_request  uri=/docs/ filename=(null) handler=(null)
main     quick_handler      uri=/docs/ filename=(null) handler=(null)
main     pre_translate_name uri=/docs/ filename=(null) handler=(null)
main     translate_name     uri=/docs/ filename=(null) handler=(null)
main     map_to_storage     uri=/docs/ filename=/tmp/htdocs/docs/ handler=(null)
main     header_parser      uri=/docs/ filename=/tmp/htdocs/docs/ handler=(null)
main     access_checker     uri=/docs/ filename=/tmp/htdocs/docs/ handler=(null)
main     type_checker       uri=/docs/ filename=/tmp/htdocs/docs/ handler=(null)
main     fixups             uri=/docs/ filename=/tmp/htdocs/docs/ handler=(null)
sub      quick_handler(lookup) uri=/docs/index.html filename=(null) handler=(null)
sub      pre_translate_name uri=/docs/index.html filename=(null) handler=(null)
sub      translate_name     uri=/docs/index.html filename=(null) handler=(null)
sub      map_to_storage     uri=/docs/index.html filename=/tmp/htdocs/docs/index.html handler=(null)
sub      type_checker       uri=/docs/index.html filename=/tmp/htdocs/docs/index.html handler=(null)
sub      fixups             uri=/docs/index.html filename=/tmp/htdocs/docs/index.html handler=(null)
main     insert_filter      uri=/docs/index.html filename=/tmp/htdocs/docs/index.html handler=(null)
main     handler            uri=/docs/index.html filename=/tmp/htdocs/docs/index.html handler=text/html
main     log_transaction    uri=/docs/index.html status=200

소스에서 읽은 것이 하나씩 보입니다.

내부 리다이렉트: 처음부터 다시

ErrorDocument 404 /errors/404.html은 ap_die_r() 안에서 처리됩니다. 오류 코드에 대한 사용자 지정 응답이 /로 시작하는 로컬 경로면, 메서드를 GET으로 바꾸고 ap_internal_redirect()를 부릅니다.

            r->method = "GET";
            r->method_number = M_GET;
            ap_internal_redirect(custom_response, r);
            /* preserve ability to see %<m in the access log */
            r->method = original_method;
            r->method_number = original_method_number;
            return;

modules/http/http_request.c 199–205행

ap_internal_redirect()의 본문은 1절과 똑같은 세 줄입니다.

    access_status = ap_run_quick_handler(new, 0);  /* Not a look-up request */
    if (access_status == DECLINED) {
        access_status = ap_process_request_internal(new);
        if (access_status == OK) {
            access_status = ap_invoke_handler(new);
        }
    }
    ap_die(access_status, new);

modules/http/http_request.c 784–791행

그 앞의 internal_internal_redirect()가 새 request_rec를 준비합니다(526–687행). 서브요청과 달리 부모와 같은 풀에 만들고(537–541행), new->prev = r, r->next = new로 둘을 잇고(560–561행), 환경 변수 이름 앞에 REDIRECT_를 붙여 넘깁니다(606행, rename_original_env()). CGI에서 보이는 REDIRECT_STATUS, REDIRECT_URL이 여기서 생깁니다. 그리고 post_read_request 훅을 다시 부릅니다(681행). 주석은 mod_setenvif와 mod_unique_id가 리다이렉트 때도 할 일이 있어서라고 설명합니다.

리다이렉트가 끝없이 이어지지 않도록 ap_is_recursion_limit_exceeded()가 깊이를 셉니다(532행). 기본값은 리다이렉트 10번, 서브요청 깊이 10입니다(include/http_core.h 134–137행). LimitInternalRecursion 디렉티브로 바꿉니다.

예제 4: 없는 파일과 ErrorDocument

/nope.html은 없는 파일입니다. 설정에 ErrorDocument 404 /errors/404.html이 있고, LogLevel core:info로 AH00128이 보이게 했습니다.

===== curl http://127.0.0.1:8080/nope.html =====
HTTP 404  body: custom 404 page
main     post_read_request  uri=/nope.html filename=(null) handler=(null)
main     quick_handler      uri=/nope.html filename=(null) handler=(null)
main     pre_translate_name uri=/nope.html filename=(null) handler=(null)
main     translate_name     uri=/nope.html filename=(null) handler=(null)
main     map_to_storage     uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     header_parser      uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     access_checker     uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     type_checker       uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     fixups             uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     insert_filter      uri=/nope.html filename=/tmp/htdocs/nope.html handler=(null)
main     handler            uri=/nope.html filename=/tmp/htdocs/nope.html handler=text/html
AH00128: File does not exist: /tmp/htdocs/nope.html
redirect post_read_request  uri=/errors/404.html filename=(null) handler=(null)
redirect quick_handler      uri=/errors/404.html filename=(null) handler=(null)
redirect pre_translate_name uri=/errors/404.html filename=(null) handler=(null)
redirect translate_name     uri=/errors/404.html filename=(null) handler=(null)
redirect map_to_storage     uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect header_parser      uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect access_checker     uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect type_checker       uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect fixups             uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect insert_filter      uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=(null)
redirect handler            uri=/errors/404.html filename=/tmp/htdocs/errors/404.html handler=text/html
main     log_transaction    uri=/nope.html status=404

헷갈리기 쉬운 곳: 접근 검사를 건너뛰는 조건

예제 3의 서브요청에는 access_checker가 없었고, 예제 4의 리다이렉트에는 있었습니다. 차이는 이 조건에서 나옵니다.

    if (r->prev && (r->prev->per_dir_config == r->per_dir_config)) {
        r->user = r->prev->user;
        r->ap_auth_type = r->prev->ap_auth_type;
    }
    else if (r->main && (r->main->per_dir_config == r->per_dir_config)) {
        r->user = r->main->user;
        r->ap_auth_type = r->main->ap_auth_type;
    }
    else {

server/request.c 319–327행

부모(r->main 또는 r->prev)와 병합된 설정이 같은 객체(포인터가 같음)면, 부모가 이미 통과한 검사이므로 사용자 정보만 물려받고 인증·권한 단계 전체를 건너뜁니다. 같은 디렉터리의 index.html을 본 서브요청은 4절의 걷기 캐시 덕분에 부모와 같은 설정 객체를 받았고, 그래서 건너뛰었습니다. /errors/ 디렉터리로 간 리다이렉트는 새로 병합된 객체를 받아 검사를 다시 했습니다. 언제 같은 객체가 나오는지는 걷기 캐시의 세부 조건(auth_internal_per_conf 등, 2176–2212행)에 달려 있어 이 글에서는 더 들어가지 않습니다. 기억할 것은 "설정이 같으면 다시 검사하지 않고, 설정이 바뀌면 반드시 다시 검사한다" 는 원칙입니다(314–318행 주석).

모듈이 "이것이 클라이언트가 보낸 원래 요청인가"를 알고 싶을 때는 ap_is_initial_req()를 씁니다. r->main도 r->prev도 없으면 참입니다(2616–2620행).

이 글에서 뺀 것

정리

개념 한 줄 요약
ap_process_async_request() quick_handler → ap_process_request_internal() → ap_invoke_handler() → ap_die_r()
quick_handler 모든 단계(접근 제어 포함)를 건너뛰는 지름길. 캐시용
ap_process_request_internal() URL 해석의 유일한 길. 훅을 차례로 돌리고 OK가 아니면 바로 돌아간다
translate_name URI → r->filename. RUN_FIRST라 mod_alias가 답하면 코어는 불리지 않는다
map_to_storage 코어가 directory walk와 file walk로 설정을 고른다
걷기와 병합 Directory(+.htaccess) → DirectoryMatch → Files → Location → If 순으로 덮어쓴다
인증·권한 설정이 정해진 뒤. Require all granted면 check_user_id·auth_checker를 건너뛴다
type_checker·fixups Content-Type 정하기, 마지막 손질(mod_dir이 여기서 일한다)
ap_invoke_handler() insert_filter 다음 handler. 핸들러 이름이 없으면 Content-Type이 이름이 된다
404 걷기가 아니라 default_handler()가 정한다
서브요청 자식 풀, r->main. 단계만 돌려 결과를 본다. mod_dir은 ap_internal_fast_redirect()로 결과를 가져온다
내부 리다이렉트 같은 풀, r->prev. post_read_request부터 핸들러까지 다시. ErrorDocument, mod_rewrite

다음에 볼 것

요청이 URL에서 파일과 핸들러까지 가는 길을 봤습니다. 핸들러가 정해졌으면 이제 응답 본문이 만들어져 흘러 나갑니다. 핸들러는 소켓에 직접 쓰지 않고 데이터를 버킷에 담아 필터의 사슬로 넘깁니다. 이 글에서 insert_filter 훅과 EOR 버킷이라고만 하고 넘어간 것들입니다. 11편: 필터와 버킷 브리게이드 — 응답은 어떻게 흘러 나가나에서 이어집니다.

Spring MVC도 같은 문제를 풉니다. Spring 21편의 DispatcherServlet은 getHandler()에서 HandlerMapping 목록에 차례로 물어 처음 답하는 쪽을 쓰는데, httpd의 RUN_FIRST 훅(translate_name, handler)과 같은 발상입니다. 처리 중 생긴 오류를 한곳(processDispatchResult())에서 받아 오류 응답으로 바꾸는 것도 ap_die_r()와 닮았습니다. 다른 점은, Spring은 URL을 자바 메서드로 바꾸고 httpd는 URL을 디스크 경로와 그 경로에 붙은 설정으로 바꾼다는 것입니다. 그래서 httpd에는 Spring에 없는 "걷기"와 설정 병합 단계가 요청마다 있습니다.

다른 글