Chapter 5. Routes and Server Modules
A route ties a path of a request to what HTTPD does with it. Without a route, HTTPD serves every path as a file. With a route, a path can go to a program (a server module), or it can stay a file and ask for a login first. This chapter shows how to write the routes in the Parmlib member, in which order, and what happens when a module is not there. The login that a route asks for is the subject of Chapter 6, “Authentication and Security”. The full syntax and the full list of messages are in ‘Routes and Authentication Policy’ in the HTTPD Reference.
The Two Kinds of Route
A route is one line in the member. There are two forms.
| Line | Form |
MOD | MOD=program [pattern] [AUTH=mode] [RES=class:resource] |
LOC | LOC=pattern [AUTH=mode] [RES=class:resource] |
Table 15. The route lines
A MOD route names a program. HTTPD links to the program, and the program answers the request. A LOC route has no program. HTTPD serves the path as a file from the document root, as it does without a route. The only difference is the policy: a LOC route can ask for a login or a RACF permission, and a path without a route cannot.
Both forms have the same options. AUTH= says how HTTPD asks a client for a login. RES= names a RACF resource that the user must be allowed to read. Both are explained in Chapter 6, “Authentication and Security”. A route without any option is public.
MOD= Routes
The first word after MOD= is the name of the load module. HTTPD changes it to upper case and keeps at most 8 characters. A longer name is cut without a message. The second word is the pattern: the path that the route claims.
MOD=MVSMF /zosmf/* AUTH=TOKENThis line sends every path that starts with /zosmf/ to the module MVSMF. The patterns are described in “The Order of the Routes and the First Match”.
If you leave out the pattern, HTTPD builds one from the program name: *. and the name in lower case. The line MOD=REXX becomes the pattern *.rexx. A pattern that starts with *. makes an extension route. For a request on an extension route HTTPD sets the variable SCRIPT_FILENAME to the document root followed by the path of the request, so that the module knows which file the client asked for. No other route gets this variable.
Each MOD line adds one route. Many routes can use the same module, as the examples in this chapter show. HTTPD reads at most 8 words of a line. Words after the eighth are ignored without a message.
LOC= Routes
A LOC route is the way to protect part of the document tree. The route does not change what HTTPD serves. It decides who may get it.
LOC=/private/* AUTH=BASIC
LOC=/pub/* AUTH=NONEThe first line asks for a user and a password for every path below /private/. The second line makes every path below /pub/ public explicitly.
Two details prevent surprises:
- A pattern that ends in
/*does not claim the directory itself. A route for/private/*does not cover the path/private. Add a plain-path route when you need that. - A line that is only options, such as
LOC=AUTH=BASIC RES=FACILITY:HTTPD.ADMIN, has no path. HTTPD does not start with such a line, because the path would be public (“Warnings and Refusals While HTTPD Reads the Routes”).
The serving of the files is described in Chapter 7, “Serving Files”.
The Order of the Routes and the First Match
HTTPD keeps all routes, MOD and LOC together, in one table in the order of the lines. For a request it walks the table from the top and uses the first route that matches. It never looks for the best match. The order of the lines is the only thing that sets the priority. The policy of the first route is the whole policy of the request. A later route that also matches is not asked.
Write the specific routes first and the general ones last.
What a Pattern Matches
HTTPD matches the path of the request, without the query string. A pattern is of one of two kinds.
| Kind | Rules |
| Plain path | The path and the pattern must be equal. The case of the letters does not count. /status matches /STATUS and does not match /status/. |
With * or ? | The whole path must match. The case of the letters counts. * stands for any characters, also a slash. At the end of the pattern it also stands for no character, so /pub/* matches /pub/. Anywhere else it stands for at least one character. ? stands for one character. |
Table 16. The two kinds of pattern
The captures below use this member. It has two plain-path routes, both protected by Basic authentication:
PORT=8080
DOCROOT=/www
REALM=MVS.EXAMPLE
LOC=/hello.txt AUTH=BASIC
LOC=/pub AUTH=BASIC
A plain path claims exactly that path and ignores the query string. The request for /hello.txt asks for a login, and so does the request with a query:
$ curl -i http://localhost:8080/hello.txt
HTTP/1.1 401 Unauthorized
Date: Tue, 27 Oct 2026 05:53:48 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
WWW-Authenticate: Basic realm="MVS.EXAMPLE"
Cache-Control: no-store
Content-Type: text/plain
Transfer-Encoding: chunked
401 Unauthorized
$ curl -i "http://localhost:8080/hello.txt?x=1"
HTTP/1.1 401 Unauthorized
Date: Tue, 27 Oct 2026 05:53:48 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
WWW-Authenticate: Basic realm="MVS.EXAMPLE"
Cache-Control: no-store
Content-Type: text/plain
Transfer-Encoding: chunked
401 Unauthorized
The route LOC=/pub does not claim a path below it. The file /pub/notice.txt is public:
$ curl -i http://localhost:8080/pub/notice.txt
HTTP/1.1 200 OK
Date: Tue, 27 Oct 2026 05:53:48 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
Content-Type: text/plain
X-Content-Type-Options: nosniff
Content-Length: 23
This notice is public.
A path that no route claims is public and is served as a file:
$ curl -i http://localhost:8080/README.txt
HTTP/1.1 200 OK
Date: Tue, 27 Oct 2026 05:53:48 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
Content-Type: text/plain
X-Content-Type-Options: nosniff
Content-Length: 36
notes without a file name extension
If you want to protect everything below a directory, write LOC=/pub/*. A route for a directory and for everything below it needs two lines, one for the directory and one with the trailing /*.
The First Line Wins
Two routes for the same path show the rule. In the first member the Basic route stands first:
PORT=8080
DOCROOT=/www
REALM=MVS.EXAMPLE
LOC=/private/* AUTH=BASIC
LOC=/private/* AUTH=NONE
The first route claims the path. The second route is never used, and the request needs a login:
$ curl -i http://localhost:8080/private/secret.txt
HTTP/1.1 401 Unauthorized
Date: Tue, 27 Oct 2026 05:50:48 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
WWW-Authenticate: Basic realm="MVS.EXAMPLE"
Cache-Control: no-store
Content-Type: text/plain
Transfer-Encoding: chunked
401 Unauthorized
In the second member the public route stands first:
PORT=8080
DOCROOT=/www
REALM=MVS.EXAMPLE
LOC=/private/* AUTH=NONE
LOC=/private/* AUTH=BASIC
Now the same request is public. The route that asks for the login is dead, and HTTPD gives no message:
$ curl -i http://localhost:8080/private/secret.txt
HTTP/1.1 200 OK
Date: Tue, 27 Oct 2026 05:50:40 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
Content-Type: text/plain
X-Content-Type-Options: nosniff
Content-Length: 43
private: any logged-in user may read this.
LOC=/* AUTH=NONE makes all routes below it unreachable. If it stands above a protected route, that route protects nothing. HTTPD does not warn about this. After a change of the routes, request the protected path without a login and check that you get the 401.The Fixed Paths
The paths /login and /logout belong to HTTPD. A request for one of them goes to the login code of HTTPD whatever the routes say. Do not write a route for them. The paths that match /login.* and /favicon.* are exempt from the login for the methods GET and HEAD, so that the login page can load its parts (Chapter 6, “Authentication and Security”).
The Standard Module: mvsMF
The module that HTTPD is mostly used for is mvsMF. It answers the REST API of z/OSMF below the path /zosmf/. mvsMF is a separate product. Its load module is called MVSMF. This is the member that the captures of this guide use. It has the routes for mvsMF, and it routes the display modules (Chapter 8, “Operating HTTPD”).
# HTTPD lab configuration (ug-ops, ug-security, ug-routes)
PORT=8080
DOCROOT=/www
REALM=MVS.EXAMPLE
MOD=MVSMF /zosmf/info AUTH=NONE
MOD=MVSMF /zosmf/services/authenticate AUTH=NONE
MOD=MVSMF /zosmf/restconsoles/* AUTH=TOKEN RES=FACILITY:MVSMF.CONSOLE
MOD=MVSMF /zosmf/* AUTH=TOKEN
MOD=HTTPDSRV /.dsrv AUTH=FORM
MOD=HTTPDM /.dm AUTH=BASIC
MOD=HTTPDMTT /.dmtt AUTH=BASIC
LOC=/pub/* AUTH=NONE
LOC=/private/* AUTH=BASIC
LOC=/form/* AUTH=FORM
LOC=/token/* AUTH=TOKEN
LOC=/admin/* AUTH=BASIC RES=FACILITY:HTTPD.ADMIN
LOC=/denied/* AUTH=BASIC RES=FACILITY:HTTPD.DENIED
Read the four MVSMF routes from top to bottom.
/zosmf/infoand/zosmf/services/authenticateareAUTH=NONE. HTTPD lets the request through, and mvsMF asks for the password itself. A request to/zosmf/infowithout a password gets its 401 from mvsMF:
$ curl -i http://localhost:8080/zosmf/info
HTTP/1.1 401 Unauthorized
Date: Tue, 27 Oct 2026 05:53:01 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
Content-Length: 0
WWW-Authenticate: Basic realm="MVS.EXAMPLE"
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
Content-Language: en
/zosmf/restconsoles/*isAUTH=TOKENand has aRES=. The user needs READ on the profileMVSMF.CONSOLEin the classFACILITY./zosmf/*isAUTH=TOKEN. It takes everything else.
The order matters. The route for the console stands before /zosmf/*, because the general route also matches the console path. If you swap the two lines, the console path falls to the general route, and the check of the resource is lost.
The meaning of TOKEN and of RES= is in Chapter 6, “Authentication and Security”.
Where the Module Must Be
HTTPD links to a module by name, in the task of the server. The system finds the module in the STEPLIB of the procedure or in the link list. A module in another library is not found. Put the module into the library that the STEPLIB names, or add the library to the STEPLIB.
Whether the library is APF-authorized decides the message HTTPD002I and the protection key in which the system fetches the module. This is described in ‘The Started Task and the Package’ in the HTTPD Reference. Because of that key, a module must not change its own static storage (Chapter 9, “Writing a Server Module”).
Modules Held in Storage: RESIDENT
By default HTTPD loads each module of a MOD route once, when it has read the member and before it serves a request. It holds the copy until it stops. A request then does not fetch the module from the library. HTTPD does this once for each program name, however many routes use the name. The member keyword is RESIDENT, with the values YES (the default) and NO.
D C shows the number of modules held in the line HTTPD137I. The member above uses four programs, and the line shows MODULES HELD 4.
A module that HTTPD cannot hold still works. Each request fetches it, as with RESIDENT=NO. The start tells you why:
| Message | Meaning |
HTTPD440W | HTTPD did not find the module at the start. It does not hold it. If the module is in the library later, the route works. |
HTTPD441W | The module is not reentrant and not serially reusable. HTTPD does not hold it. |
HTTPD442W | The load failed. The message gives the abend code and the reason code. |
Table 17. The messages for resident modules
With RESIDENT=NO HTTPD does not look at the modules at the start. A missing module is then silent until the first request (see “A Route to a Missing Module”).
Context Blocks for Modules
A server module must not keep data in writable static storage. When a module needs data that lives from one request to the next, it asks HTTPD for a context block. The module calls the function http_cgictx_get with an 8-byte name (an eyecatcher) and a size. The first call for a name creates the block. Every later call with the same name, from any worker, returns the same block. The block stays until the server stops. HTTPD never frees it.
The keyword CGI_CONTEXT_POINTERS is the size of the table in which HTTPD records these blocks. Each name takes one entry. The table has one entry more than the number. The default is 255, which is also the largest value. If the table is full, the call returns no block (NULL) and the module has to deal with that. A module does not need the keyword to run. Leave it at the default unless the documentation of a module tells you to change it. How a module uses the block is described in Chapter 9, “Writing a Server Module” and in ‘The Server Module Interface’ in the HTTPD Reference.
A Route to a Missing Module
A route to a module that is not in the library is not an error of the member. HTTPD registers the route and tells you at the start:
PORT=8080
DOCROOT=/www
MOD=NOSUCH /nosuch/* AUTH=NONE
S HTTPD,M=PRMNOSU
$HASP100 HTTPD ON STCINRDR
$HASP373 HTTPD STARTED
IEF403I HTTPD - STARTED - TIME=05.34.49
HTTPD000I HTTPD 4.5.1 (nnnnnnn) STARTING
HTTPD005I LIBC370 2.26.0 (3AA3B28)
HTTPD002I AUTHORIZED BY SVC (MODULE KEY 8)
HTTPD004I STC IDENTITY SET TO HTTPD/USER VIA RACINIT
RAKF0004 INVALID ATTEMPT TO ACCESS SYSTEM: USER:HTTPPRB
HTTPD036I MODULE NOSUCH REGISTERED FOR /nosuch/*
HTTPD054I LISTENING ON ANY PORT 8080
HTTPD440W MODULE NOSUCH NOT FOUND AT STARTUP, NOT HELD
HTTPD061I STARTING SOCKET THREAD TCB(9CDB00) TASK(100FC8) STACKSIZE(32768)
HTTPD061I STARTING WORKER(0DD7F0) TCB(9CD410) TASK(114FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD100) TCB(9CD0E0) TASK(125FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD080) TCB(9BBCF8) TASK(136FC8) STACKSIZE(65536)
HTTPD001I HTTPD 4.5.1 READY - SERVING /www
IEA703I 806- 4 HTTPD HTTPD MODULE ACCESSED NOSUCH
HTTPD908E EXTERNAL PROGRAM NOSUCH was not found
The message HTTPD036I says that the route exists. The message HTTPD440W says that the module is not found and not held. The server starts. The failure comes with the first request:
$ curl -i http://localhost:8080/nosuch/x
HTTP/1.1 503 Service Unavailable
Date: Tue, 27 Oct 2026 05:50:25 GMT
Server: HTTPD Server
X-Powered-By: MVSLOVERS
Connection: keep-alive
Content-Type: text/plain
Transfer-Encoding: chunked
External program NOSUCH was not found
The client gets the status 503 and the text External program NOSUCH was. The console shows the system message
not foundIEA703I 806- 4 and, after it, HTTPD908E (the last two lines of the start capture above). The completion code S806 means that the system found the module neither in the STEPLIB nor in the link list. If you see this, check the name in the route, then the library (“Where the Module Must Be”). The other reasons for a failure are listed in ‘Routes and Authentication Policy’ in the HTTPD Reference.
With RESIDENT=NO the start is silent about the module:
PORT=8080
DOCROOT=/www
RESIDENT=NO
MOD=NOSUCH /nosuch/* AUTH=NONE
S HTTPD,M=PRMRESN
$HASP100 HTTPD ON STCINRDR
$HASP373 HTTPD STARTED
IEF403I HTTPD - STARTED - TIME=05.34.34
HTTPD000I HTTPD 4.5.1 (nnnnnnn) STARTING
HTTPD005I LIBC370 2.26.0 (3AA3B28)
HTTPD002I AUTHORIZED BY SVC (MODULE KEY 8)
HTTPD004I STC IDENTITY SET TO HTTPD/USER VIA RACINIT
RAKF0004 INVALID ATTEMPT TO ACCESS SYSTEM: USER:HTTPPRB
HTTPD036I MODULE NOSUCH REGISTERED FOR /nosuch/*
HTTPD054I LISTENING ON ANY PORT 8080
HTTPD061I STARTING SOCKET THREAD TCB(9CDB00) TASK(100FC8) STACKSIZE(32768)
HTTPD061I STARTING WORKER(0DD7F0) TCB(9CD410) TASK(114FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD100) TCB(9CD0E0) TASK(125FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD080) TCB(9BBCF8) TASK(136FC8) STACKSIZE(65536)
HTTPD001I HTTPD 4.5.1 READY - SERVING /www
Warnings and Refusals While HTTPD Reads the Routes
HTTPD reads the whole member before it decides whether to start. Most mistakes in a route give a warning, and the route is registered. One example shows the common ones. The member has a bad RESIDENT value and four routes with options that HTTPD does not accept:
PORT=8080
DOCROOT=/www
RESIDENT=MAYBE
MOD=NOSUCH /nosuch/* AUTH=NONE
LOC=/a/* AUTH=NONE RES=FACILITY:HTTPD.ADMIN
LOC=/b/* AUTH=BASIC RES=NOCOLON
LOC=/c/* AUTH=NONE BOGUS=1
LOC=/d/* RES=FACILITY:HTTPD.NOPROFILE
S HTTPD,M=PRMRTE
$HASP100 HTTPD ON STCINRDR
$HASP373 HTTPD STARTED
IEF403I HTTPD - STARTED - TIME=05.35.14
HTTPD000I HTTPD 4.5.1 (nnnnnnn) STARTING
HTTPD005I LIBC370 2.26.0 (3AA3B28)
HTTPD002I AUTHORIZED BY SVC (MODULE KEY 8)
HTTPD004I STC IDENTITY SET TO HTTPD/USER VIA RACINIT
RAKF0004 INVALID ATTEMPT TO ACCESS SYSTEM: USER:HTTPPRB
HTTPD443W INVALID RESIDENT VALUE "MAYBE" (YES|NO)
HTTPD036I MODULE NOSUCH REGISTERED FOR /nosuch/*
HTTPD414W AUTH=NONE IGNORES RES=FACILITY:HTTPD.ADMIN (PUBLIC ROUTE)
HTTPD417I LOCATION /a/* REGISTERED
HTTPD412W IGNORING MALFORMED RES= 'NOCOLON' (NEED CLASS:RESOURCE)
HTTPD417I LOCATION /b/* REGISTERED
HTTPD413W IGNORING UNKNOWN ROUTE OPTION 'BOGUS=1'
HTTPD417I LOCATION /c/* REGISTERED
HTTPD417I LOCATION /d/* REGISTERED
HTTPD054I LISTENING ON ANY PORT 8080
HTTPD425W NO PROFILE FOR FACILITY:HTTPD.NOPROFILE -- /d/* NOT GATED
HTTPD440W MODULE NOSUCH NOT FOUND AT STARTUP, NOT HELD
HTTPD061I STARTING SOCKET THREAD TCB(9CDB00) TASK(100FC8) STACKSIZE(32768)
HTTPD061I STARTING WORKER(0DD188) TCB(9CD410) TASK(114FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD088) TCB(9CD0E0) TASK(125FC8) STACKSIZE(65536)
HTTPD061I STARTING WORKER(0DD008) TCB(9BBCF8) TASK(136FC8) STACKSIZE(65536)
HTTPD001I HTTPD 4.5.1 READY - SERVING /www
Read the console lines one after the other.
HTTPD443Wis the wrongRESIDENTvalue. The earlier value stays.HTTPD414Wis for/a/*. The route is public (AUTH=NONE), so HTTPD ignores theRES=.HTTPD412Wis for/b/*. The textNOCOLONhas no colon. HTTPD ignores theRES=, and the route hasAUTH=BASIC.HTTPD413Wis for/c/*. HTTPD does not know the wordBOGUS=1and ignores it.HTTPD425Wis for/d/*. The route has aRES=and noAUTH=, so it gets Basic authentication. But the security product has no profile forFACILITY:HTTPD.NOPROFILE, so any user who is logged in passes. The text says the route isNOT GATED. A mistake in the resource name gives the same message (Chapter 6, “Authentication and Security”). A class that RAKF does not define gives the same message with RAKF 1.2.x (measured on TK5). With RAKF 2.0.0 (MVS/CE) it gives no message and a 403 for every request. WithRES_NOPROFILE=DENYin the member the message isHTTPD426Wand the route refuses every request (“Refusing a Route Whose Profile Is Missing”).HTTPD440Wis the missing module of the first route.
HTTPD checks only the words it knows. A line such as MOD=ADMIN /admin/* AUTH BASIC has the words AUTH and BASIC separately. HTTPD gives HTTPD413W twice and registers a public route. Look for HTTPD413W in the log after every change.
In some cases HTTPD refuses to start, because a route that asked for a login would otherwise turn public. An AUTH= value that does not exist gives HTTPD411E, HTTPD419E and HTTPD420E, and the return code is 8. The capture for that is in Chapter 4, “Configuring HTTPD”. A line in which an option stands where the program name or the path belongs also refuses the start, if it asks for a login or a resource. A line with only AUTH=NONE, or with no option, gives HTTPD421W for MOD or HTTPD415W for LOC, and the start goes on.
A change in a route takes effect when you stop and start HTTPD. To look at the routes that run now, use the display module HTTPDSRV (Chapter 8, “Operating HTTPD”).