Files stored as BLOBs can be served straight from the database. ORDS's media resource source type takes a query that returns a MIME type and the content, and sends the content with that type, so a browser shows an image or opens a CSV. For a download with a proper file name, a small PL/SQL handler adds a Content-Disposition header.
Before You Start
You need ORDS installed and running against your database, and a schema to work in. The examples use NIMBUS, the sample schema of a fictional airline, which you can install with the scripts in the setup/nimbus folder of the Oracle Database 26ai code repository on GitHub. The schema comes from Oracle Database 26ai SQL and PL/SQL Book.
ORDS in the examples answers at https://localhost:8443/ords/, and NIMBUS is REST-enabled with the URL alias nimbus. Replace the host and port with your own ORDS address. The curl commands use -k because the test server has a self-signed certificate; leave it out when your certificate is trusted. JSON responses are formatted for reading; ORDS returns them on one line.
The files come from the OFFER_FILES table filled in How to Upload Files with an ORDS POST Handler.
Syntax
ords.define_handler(..., p_method => 'GET',
p_source_type => ords.source_type_media,
p_source => 'select mime_type, content from ... where id = :id');The query returns two columns: the MIME type first, then the BLOB or CLOB.
Serve Files with the Media Source Type
Example:
begin
ords.define_template(p_module_name => 'lounge', p_pattern => 'files/:id');
ords.define_handler(p_module_name => 'lounge',
p_pattern => 'files/:id',
p_method => 'GET',
p_source_type => ords.source_type_media,
p_source => 'select mime_type, content
from offer_files
where file_id = :id');
commit;
end;
/Example:
curl -k -D - -o downloaded.png https://localhost:8443/ords/nimbus/lounge/files/1
Output (headers, without the Date header):
HTTP/1.1 200 OK Content-Type: image/png ETag: "sPBHJz3fza1TSte7miKs0/Jxrdg+9PVc08qySnhPDtViA314MUJFnJJfQ5BE1xnMN4GTyMUfJJ+/selfA8m2mw==" Transfer-Encoding: chunked
The downloaded file is byte for byte the file that was uploaded. Opened in a browser, the URL shows the image:

File 2 is the CSV, sent as text/csv:
Output:
HTTP/1.1 200 OK Content-Type: text/csv ETag: "hZloVcBMmUHRcuHEVpxdCK+GLjTSP9BWGnqJtxkJdGiZEFhzeOguYd2/q5Kl96tAqnEgydYR6J5WkHzMXODeVg==" Transfer-Encoding: chunked airport_code,price_date,usd_per_gallon DXB,2026-01-05,2.41 LHR,2026-01-05,2.88 SIN,2026-01-05,2.52 JFK,2026-01-05,2.63 DXB,2026-02-02,2.37 LHR,2026-02-02,2.91 SIN,2026-02-02,2.49 JFK,2026-02-02,2.70 DXB,2026-03-02,2.45 LHR,2026-03-02,2.95 SIN,2026-03-02,2.55 JFK,2026-03-02,2.74
A file ID that does not exist returns 404 Not Found.
Download with a File Name
The media source type sends no file name, so browsers save the file under the last part of the URL. A PL/SQL handler can send its own headers, including Content-Disposition, and then the BLOB with WPG_DOCLOAD.DOWNLOAD_FILE:
Example:
begin
ords.define_template(p_module_name => 'lounge', p_pattern => 'files/:id/download');
ords.define_handler(p_module_name => 'lounge',
p_pattern => 'files/:id/download',
p_method => 'GET',
p_source_type => ords.source_type_plsql,
p_source => q'[
declare
v_name offer_files.file_name%type;
v_type offer_files.mime_type%type;
v_file blob;
begin
select file_name, mime_type, content into v_name, v_type, v_file
from offer_files where file_id = :id;
owa_util.mime_header(v_type, false);
htp.p('Content-Disposition: attachment; filename="' || v_name || '"');
htp.p('Content-Length: ' || dbms_lob.getlength(v_file));
owa_util.http_header_close;
wpg_docload.download_file(v_file);
exception
when no_data_found then
:status_code := 404;
end;]');
commit;
end;
/Example:
curl -k -D - -o offer.png https://localhost:8443/ords/nimbus/lounge/files/1/download
Output (headers, without the Date header):
HTTP/1.1 200 OK Content-Type: image/png Content-Disposition: attachment; filename="dxb-lounge-offer.png"; filename*=UTF-8''dxb-lounge-offer.png ETag: "y/HCbcKRulscNILzU0yMYF7kzYp6++kGyQzOFGJl6EH0Xa2nniyY2TJ6NsHLRvf0QN64toKNA3lYGgnwmJnWDQ==" Transfer-Encoding: chunked
ORDS added an encoded filename* form next to the plain file name, and browsers now save the file as dxb-lounge-offer.png.
Things to Know
- ORDS adds an ETag to media responses, so clients can cache files and revalidate them.
- Use inline instead of attachment in Content-Disposition to suggest a file name but still display the file.
- Store the MIME type with each file; guessing it on the way out leads to files that browsers refuse to show.
Related Guides
Conclusion
The ORDS media resource source type serves a BLOB or CLOB with its MIME type from a two-column query. For downloads with a file name, write the headers and the BLOB yourself in a PL/SQL handler with OWA_UTIL and WPG_DOCLOAD.
