How to Download Files from ORDS (media resources)

Serve stored files over REST with the right MIME type, show images in the browser, and send downloads with a proper file name.

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:

Browser showing the PNG lounge offer banner served by the ORDS media handler files/1
GET /ords/nimbus/lounge/files/1 in a browser: the PNG stored in OFFER_FILES.

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.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted