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.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

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