Skip to content

Commit

Permalink
Add better support for adding form file parameters to web requests
Browse files Browse the repository at this point in the history
Something I ended up needing in the course of BSS work.

The previous implementation would treat files as a straight dictionary,
as in you could have only one file with a given parameter name in a
request. This isn't how `multipart/form-data` really works; you're
allowed to send multiple files in scope of a single parameter.

Additionally, the request did not permit specifying a filename, which is
yet another thing I need for BSS purposes.

I don't believe this method has any real consumers, yet anyhow:

Breaking changes
----------------

`WebRequest.AddFile()` has been altered to provide an API closer to what
the HTTP specs intended, and to allow sending multiple files for a given
request parameter. To that end:

- The signature of the method has changed from
  ```csharp
  public void AddFile(string name, byte[] data);
  ```
  to
  ```csharp
  public void AddFile(string paramName, byte[] data, string filename = "blob");
  ```

- Calling `.AddFile()` with the same first parameter (formerly called
  `name`, now called `paramName`) twice will result in two files being
  sent in the request, rather than the second call overwriting the file
  added by the first one.

- If the `filename` parameter is not explicitly specified in the method
  call, the filename given in the `Content-Disposition` header of the
  multipart request has changed from being equal to the parameter name
  to `blob`. This is consistent with JavaScript `FormData` API
  behaviour.
  • Loading branch information
bdach committed Nov 6, 2024
1 parent b8e9e0b commit c7d1e72
Showing 1 changed file with 14 additions and 8 deletions.
22 changes: 14 additions & 8 deletions osu.Framework/IO/Network/WebRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public class WebRequest : IDisposable
/// <summary>
/// FILE parameters.
/// </summary>
private readonly IDictionary<string, byte[]> files = new Dictionary<string, byte[]>();
private readonly List<FormFile> files = new List<FormFile>();

/// <summary>
/// The request headers.
Expand Down Expand Up @@ -349,9 +349,9 @@ private async Task internalPerform(CancellationToken cancellationToken = default

foreach (var p in files)
{
var byteContent = new ByteArrayContent(p.Value);
var byteContent = new ByteArrayContent(p.Content);
byteContent.Headers.Add("Content-Type", "application/octet-stream");
formData.Add(byteContent, p.Key, p.Key);
formData.Add(byteContent, p.ParamName, p.Filename);
}

postContent = await formData.ReadAsStreamAsync(linkedToken.Token).ConfigureAwait(false);
Expand Down Expand Up @@ -662,17 +662,21 @@ public void AddRaw(Stream stream)
}

/// <summary>
/// Add a new FILE parameter to this request. Replaces any existing file with the same name.
/// Add a new FILE parameter to this request.
/// This may not be used in conjunction with <see cref="AddRaw(Stream)"/>. GET requests may not contain files.
/// </summary>
/// <param name="name">The name of the file. This becomes the name of the file in a multi-part form POST content.</param>
/// <param name="paramName">The name of the form parameter of the request that the file relates to.</param>
/// <param name="data">The file data.</param>
public void AddFile(string name, byte[] data)
/// <param name="filename">
/// The filename of the file to be sent to be reported to the server in the <c>Content-Disposition</c> header.
/// <c>blob</c> is used by default if omitted, to <see href="https://developer.mozilla.org/en-US/docs/Web/API/FormData/append#filename">mirror browser behaviour</see>.
/// </param>
public void AddFile(string paramName, byte[] data, string filename = "blob")
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(paramName);
ArgumentNullException.ThrowIfNull(data);

files[name] = data;
files.Add(new FormFile(paramName, data, filename));
}

/// <summary>
Expand Down Expand Up @@ -931,5 +935,7 @@ protected override void Dispose(bool disposing)
baseStream.Dispose();
}
}

private record struct FormFile(string ParamName, byte[] Content, string Filename);
}
}

0 comments on commit c7d1e72

Please sign in to comment.