Files
About Http.File
@typespec/http
provides a special model, [TypeSpec.Http.File
][typespec-http-file], that represents the concept of a file. The HTTP library has special behavior when a request, response, or multipart part body is-or-extends TypeSpec.Http.File
.
using TypeSpec.Http;
op exampleDownload(): File;
If File
were any other ordinary model, the above operation would be interpreted as returning a structured JSON data object that represents the fields of the model. File
, however, has special semantics. The HTTP library understands that an operation that returns a File
or any model that extends File
has the semantics of downloading a file with binary payload and arbitrary content-type from the server. We call these cases “file bodies” to distinguish them from ordinary bodies.
Http.File
has three properties that are understood to have special meaning when a request, response, or multipart payload has a file body.
contents
: the contents of the file, which are the body of the request, response, or multipart payload. This location cannot be changed by subtypes ofHttp.File
.contentType
: (optional) the media (MIME) type of the file, which is sent in theContent-Type
header of the request, response, or multipart payload. This location cannot be changed by subtypes ofHttp.File
.filename
: (optional) the name of the file, which is sent in thefilename
parameter of theContent-Disposition
header of response and multipart payloads. By default, it cannot be sent in requests, asContent-Disposition
is only valid for response and multipart payloads. This location can be changed by subtypes ofHttp.File
that apply HTTP metadata to the location. See Overriding thefilename
location below for more information.
Using Http.File
in operations
An operation payload (request, response, or multipart part) has a file body if:
- The type of the body is effectively a model that is or extends
Http.File
AND - there is no explicit declaration of a
Content-Type
header (seeFile
with an explicitContent-Type
header below for reasoning and more information).
By “effectively a model that is or extends Http.File
,” we mean cases where an explicit body property is provided and its type is or extends Http.File
as well as cases where Http.File
is spread into a request or response payload or Http.File
is intersected with other models in a request or response and the only non-metadata properties in the payload are properties of File
(see When a model is effectively a File
below for a more precise description with examples). The following sections contain examples of using Http.File
in various contexts to define operations that have file bodies.
Downloading a file
All of the following TypeSpec operation definitions have file bodies in the response:
// The response is _exactly_ a File, so the response has a file body.op download(): File;
// The response has an explicit body that is a File, so the response has a file body.op download(): { @bodyRoot file: File;};
// The response is _effectively_ a File (`File` is the only thing spread into it), so the response has a file body.op download(): { ...File;};
// File is intersected with other models containing only HTTP metadata, so the response has a file body.op download(): OkResponse & File;
// The response has an explicit body that is _effectively_ a File, so the response has a file body.op download(): { @bodyRoot file: { ...File; };};
Uploading a file
All of the following TypeSpec operation definitions have file bodies in the request:
// The request has an explicit body that is _exactly_ a File, so the request has a file body.op upload(@bodyRoot file: File): void;
alias FileRequest = { @header("x-request-id") requestId: string;} & File;
// File is intersected with other models containing only HTTP metadata, so the request has a file body.op upload(...FileRequest): void;
// The request is _effectively_ a File (`File` is the only thing spread into it), so the request has a file body.op upload(...File): void;
// The request has an explicit body that is _effectively_ a File, so the request has a file body.op upload( @bodyRoot body: { ...File; },): void;
Using files in multipart payloads
Multipart payloads are commonly used to upload files (e.g. in HTML forms). To declare a multipart part that has a file body, ensure the part’s type follows the same rules as for request and response payloads: it must either be a type that is effectively an instance of Http.File
, or must have an explicit body property that effectively is-or-extends Http.File
. All of the following examples declare multipart parts that have file bodies:
// The type of the form-data part is _exactly_ a File, so the part has a file body.op multipartUpload( @multipartBody fields: { file: HttpPart<File>; },): void;
// The type of the form-data part has an explicit body that is _exactly_ a File, so the part has a file body.op multipartUpload( @multipartBody fields: { file: HttpPart<{ @bodyRoot file: File; }>; },): void;
// The type of the mixed part is _exactly_ a File, so the part has a file body.op multipartMixedDownload(): { @multipartBody data: [HttpPart<File>];};
// The type of the mixed part has an explicit body that is _exactly_ a File, so the part has a file body.op multipartMixedDownload(): { @multipartBody data: [ HttpPart<{ @bodyRoot file: File; }> ];};
All of the above examples will also have file bodies if File
is replaced with a model that extends File
or a model that is effectively File
(e.g. {...File}
).
You can also mix and match parts that have file bodies with other parts. The following TypeSpec gives a more comprehensive example of uploading data alongside files:
model Widget { id: string; name: string; weight: float64;}
op multipartUpload( @multipartBody fields: { // The widget is uploaded in a part named `widget` and uses form-urlencoded serialization. widget: HttpPart<{ @header contentType: "application/x-www-form-urlencoded"; @body widget: Widget; }>;
// The part named `attachments` can be sent multiple times, and each `attachments` part has a file body. attachments: HttpPart<File>[]; },): void;
For more information about the handling of multipart payloads in @typespec/http
, see Multipart.
When a model is effectively a File
In the above sections, we used the idea of “effective” files. In the context of an HTTP operation, a model is effectively a file if it has all of the properties of Http.File
(true properties of Http.File
from a spread or intersection, not just properties that have the same shape as a File
) AND after removing all of the applicable metadata properties, it has only properties of Http.File
.
- A property of
Http.File
means a property that is actually sourced from theHttp.File
model, e.g. through spreadingFile
into another model or usingmodel is
syntax. - Applicable metadata means an HTTP metadata decorator that applies in context. For example,
@path
is applicable in requests, but not response or multipart payloads.@statusCode
is applicable in responses, but not request or multipart payloads.
The following table shows which metadata annotations are applicable in which contexts:
Metadata | @header | @query | @statusCode | @path |
---|---|---|---|---|
Request | ✅ | ✅ | ❌ | ✅ |
Response | ✅ | ❌ | ✅ | ❌ |
Multipart | ✅ | ❌ | ❌ | ❌ |
Examples that are effectively a File
// The parameters of this operation are effectively a file because the @header parameter// is not considered when checking if the request is a fileop uploadFileWithHeader(@header("x-request-id") requestId: string, ...Http.File): void;
model CommonParameters { @query("api-version") apiVersion: string; @header("x-request-id") requestId: string;}
// The parameters of this operation are effectively a file because the common parameters// are all applicable metadata and not considered when checking if the request is a fileop uploadFileWithCommonParams(...CommonParameters, ...File): void;
// The response has a file body because the `@statusCode` property is not considered when// checking if the response is a fileop downloadFileWithStatusCode(@path name: string): { @statusCode _: 200; ...File;};
// The response has a file body because the `OkResponse` model only has response-applicable// metadata that is not considered when checking if the response is a fileop downloadFileWithIntersection(@path name: string): OkResponse & File;
model OpenAPIFile extends File<"application/json" | "application/yaml", string> { @path filename: string;}
// The response and request have file bodies because the common parameters are all// applicable metadata in the request, and the `OkResponse` model only contains// applicable metadata for the response.op uploadAndDownload(...CommonParameters, ...OpenAPIFile): OkResponse & OpenAPIFile;
model FileData { @header("x-created") created: utcDateTime; ...File;}
// The request has a file body because the `created` header is applicable metadata for// responses, and the rest of `FileData` is the properties of `File`.op upload(@bodyRoot file: FileData): OkResponse;
// The response has a file body because the `OkResponse` model only contains applicable// metadata for the response, and the `created` header is also applicable in the response.// The properties that are left over are the properties of `File`.op download(): OkResponse & FileData;
Examples that are not effectively a File
// The request does not have a file body because the `userId` parameter is a body property,// so this will cause the `File` to be serialized as JSON in the request.op uploadFileWithExtraParam(userId: string, ...File): void;
model FileData { @query created: utcDateTime; ...File;}
// The response does not have a file body because `@query` metadata is not applicable// in responses, so the `created` property is placed in the body and the whole `FileData`// model is serialized as JSON.op download(): FileData;
model OpenAPIFile extends File<"application/json" | "application/yaml", string> { @path filename: string;}
model OpenAPIFileResponse { @statusCode statusCode: 200; ...SpecFile;}
// The request does not have a file body because the `statusCode` property is not// applicable metadata for requests, so the request body would be serialized as a JSON// object. The same model _would_ create a file body in a response, though.op upload(@bodyRoot data: OpenAPIFileResponse): void;
Creating custom File
models
You can declare custom types of files by providing arguments to the Http.File
template or extending it. Custom files can be used to add additional constraints on the contents of files or to override the location metadata of the filename
property. For example, to declare a file that can contain PNG or JPEG images:
alias ImageFile = File<"image/png" | "image/jpeg">;
// or
model ImageFile extends File<"image/png" | "image/jpeg"> {}
// or
model ImageFile extends File { contentType: "image/png" | "image/jpeg";}
The above examples are equivalent ways to narrow the allowed media types of the file’s contents. For convenience, you can specify the ContentType
parameter of the File
template inline, or you can override the type of the property in your own model that extends File
.
The extra contentType
information in these custom files provides an extra contractual guarantee about what kinds of data can be inside the file. In the above case, it is guaranteed to be either PNG or JPEG image data. The allowed Content-Type
header values for the payload are also restricted to only allow those values that satisfy the contentType
property’s type.
NOTE: While you can override the type of properties within Http.File
by extending it, you cannot define additional properties.
Overriding the filename
location
By default, the filename
is located in the Content-Disposition
header of response and multipart payloads, but that header is not valid for request payloads. If you wish to send the filename
in a request, you must override the location. For example, the follwing TypeSpec defines an OpenAPIFile
in which the filename
is appended to the route path when a file is uploaded, but since @path
only applies to requests, the filename
will still be returned in the Content-Dispotion
header in responses or multipart payloads:
model OpenAPIFile extends File<"application/json" | "application/yaml"> { @path filename: string;}
@route("/specs")interface Specs { upload(@bodyRoot file: OpenAPIFile): void;
download(@path name: string): OpenAPIFile;}
NOTE: Header metadata is applicable in all contexts, so if you use a custom header (e.g. @header("x-filename") filename: string
) in your custom file, beware that it will apply to request, response, and multipart payloads equally.
Textual files
The File
template accepts a Contents
argument that may be TypeSpec.string
, TypeSpec.bytes
, or any scalar that extends them. If the Contents
argument is or extends TypeSpec.string
, the file is considered a textual file. For example:
// Since `Contents` is `string`, this file type can only contain text data.alias TextFile = File<Contents = string>;
// This file type can only contain text and is guaranteed to have `contentType: "application/yaml"`.model YamlFile extends File<"application/yaml", string> {}
// This file is another way to declare YamlFile by overriding the type of `contentType`model YamlFile extends File<Contents = string> { contentType: "application/yaml";}
Textual files provide an extra contractual guarantee that the contents of the file must be text (i.e. the contents can be represented as a string
).
NOTE: TypeSpec does not prescribe any specific text encoding. Emitters and libraries should take care to honor the charset
of the file if one is specified, and should assume UTF-8 encoding in the absence of any protocol-level indication of the text encoding on the wire.
Http.File
as a structured model
In other cases, when Http.File
is not itself the body of a request or response, it is treated as a structured model just like any other ordinary model. The TypeSpec HTTP library will generally warn you in cases where the File
looks like it might indicate a file body, but does not because of the library’s rules.
File
properties inside other models
If a property of a model is a File
, and that model is serialized as JSON, the structure of the File will be serialized as JSON inline, with the contents encoded as Base64 data. For example, in the following operation:
model Example { id: string; attachment?: File;}
op getExample(@path id: string): Example;
The response body with the File
serialized as JSON looks like:
{ "id": "<string>", "attachment": { "contentType": "<string?>", "filename": "<string?>", "contents": "<base64>" }}
File
inside a union
If File
is a variant of a union in an exact body, it is not treated as a file body. For example:
// Warning: An HTTP File in a union is serialized as a structured model// instead of being treated as the contents of a file...op uploadFileOrString(@path id: string, @body data: File | string): void;
The above operation accepts either a text/plain
string or a JSON-serialized File
object body, not a file body. To declare a single operation that accepts either a text/plain
string or a file body, declare two separate operations using @sharedRoute
:
@sharedRouteop uploadFile(@path id: string, @body data: File): void;
@sharedRouteop uploadString(@path id: string, @body data: string): void;
File
can be in a union in an HTTP response and still create a file body, but only if the union is itself the return type and not in an explicit body property. The HTTP library recognizes the variants of a union that is returned from an operation as individual and separate responses, and it is allowed to have a response type that is a File
alongside other non-file responses, but if a single response has a type that is a union that contains file, the same warning as above will appear and the File
will be treated as a structured model:
// This is allowed and creates a file body, as `File` and `string` are considered separate responses, so// this operation has two responses; the first has a file body, and the second has a `text/plain` string body.op downloadFileOrString(): File | string;
// The following does not create a file body, as it is only one response where the body of that single response// may be either a file or a string.
// Warning: An HTTP File in a union is serialized as a structured model// instead of being treated as the contents of a file...op downloadFileOrString(): { @bodyRoot data: File | string;};
File
with an explicit Content-Type
header
Operations are only considered to have file bodies if there is no explicit declaration of a Content-Type
header in the payload. If an explicit Content-Type
header is present, the File
is always considered a structured model and is not treated as a file body. The following operation does not have a file body:
// Warning: HTTP File body is serialized as a structured model in 'application/json' instead of being// treated as the contents of a file because an explicit Content-Type header is defined.op download(): { @header contentType: "application/json"; @body file: File<Contents = string>;};
The explicit Content-Type
header is not merely metadata. The HTTP library treats this header declaration as a directive about how to serialize the body. In other words, the operation above says “serialize the type of the body as JSON, and the type of the body is Http.File
.” To declare an operation with a file body, where the file can only contain JSON data, provide the ContentType
argument to the File
template instead:
op download(): File<"application/json", string>;
Similarly, and to maintain consistency, you cannot use an explicit Content-Type
header to declare the content-type of binary files:
// Warning: HTTP File body is serialized as a structured model in 'image/png, image/jpeg' instead of being// treated as the contents of a file because an explicit Content-Type header is defined.op downloadImage(): { @header contentType: "image/png" | "image/jpeg"; @body file: File;};
// Do this insteadop downloadImage(): File<"image/png" | "image/jpeg">;
Library and emitter authoring notes
For library/emitter developers working with the @typespec/http
programmatic API, you can always determine if an operation request, response, or multipart payload is a file body by checking if the bodyKind
of an HttpPayloadBody
is "file"
. If the body kind is "single"
(or any other kind), then the body is not a file body.
File bodies require special handling to account for the special nature of files. When processing file bodies:
- Assume that the
contents
of the file are always transmitted in the body without any further encoding. - Assume that the
contentType
of the file always comes from theContent-Type
header of the corresponding request, response, or multipart payload. - The
filename
should come from theContent-Disposition
header of the response or multipart payload (there is nofilename
in requests by default), but be aware that spec authors may override this location using HTTP property metadata decorators like@query
or@header
. - The
isText
field of theHttpOperationFileBody
will betrue
if the file is contractually guaranteed to only contain text (i.e. if thecontents
property has a type that is or extendsTypeSpec.string
). Textual files are a subset of binary files, with the guarantee that the contents are plain text that can be converted to astring
. See Textual files above.
See the reference documentation of HttpPayloadBody
and HttpOperationFileBody
for more information.