Dependency-free HTTP API framework for Unity, with main-thread-safe routing.
Pre-alpha (v0.0.1). The public API is unstable and may change between 0.x releases.
- Zero external dependencies — built on the .NET BCL only. Works with or without UniTask.
Task<T>based async — modern C#async/await, no coroutines required.- Main-thread dispatch built in —
await ctx.MainThreadAsync(...)to safely touch Unity APIs from a request handler. - Routing with named parameters —
/api/donors/{type}, with URL-decoded values (Korean / non-ASCII names work). - File serving with HTTP Range — videos seekable in browsers, resumable downloads, no memory bloat.
- 404 / 405 distinction and built-in
OPTIONSpreflight for CORS. - Unity 2022.3+ — verified on Mono; IL2CPP standalone should work (see platform matrix below).
In Unity: Window → Package Manager → + → Add package from git URL → paste:
https://codeberg.org/degubites/UniAPI.git
Or pin to a specific release tag:
https://codeberg.org/degubites/UniAPI.git#v0.0.1
Or edit Packages/manifest.json directly:
{
"dependencies": {
"com.degubites.uniapi": "https://codeberg.org/degubites/UniAPI.git#v0.0.1"
}
}Pending submission review. Once available, you'll be able to install through OpenUPM's scoped registry.
using Degubites.UniAPI;
using UnityEngine;
public class Bootstrap : MonoBehaviour
{
void Start()
{
var server = gameObject.AddComponent<UniAPIServer>();
server.Port = 8080;
server.MapGet("/api/ping", ctx => ctx.Text("pong"));
server.MapPost("/api/donors/{type}", async ctx =>
{
var type = ctx.RouteValues["type"];
var body = await ctx.ReadBodyAsTextAsync();
// Worker thread → main thread → worker thread, transparent.
await ctx.MainThreadAsync(() => Debug.Log($"Added donor type={type}"));
await ctx.Text("{\"result\":\"ok\"}",
contentType: "application/json; charset=utf-8");
});
server.StartServer();
}
}Verify:
curl http://localhost:8080/api/ping
# pongPattern forms:
- Static:
/api/ping - Single parameter:
/api/donors/{type}→ctx.RouteValues["type"] - Multiple parameters:
/users/{id}/posts/{postId}
Method handlers: MapGet, MapPost, MapPut, MapDelete, MapPatch. All chainable (each returns the server).
Matching policy in v0.0.1:
- First registered wins on overlap. Register literal paths before parameterized ones if both could match:
server.MapGet("/users/me", ...); // literal — register first server.MapGet("/users/{id}", ...); // parameter
- Case-sensitive on literal segments.
- Parameter values are URL-decoded.
- Wildcards (
{*rest}) and constraints ({id:int}) are not in v0.0.1 — see roadmap.
Mismatches:
- Path doesn't match anything → 404
- Path matches but for a different method → 405
ServeFileAsync streams from disk with HTTP Range support — videos seekable, downloads resumable, no memory bloat.
using System.IO;
using UnityEngine;
string mediaRoot = Application.streamingAssetsPath;
server.MapGet("/files/{name}", async ctx =>
{
var name = ctx.RouteValues["name"];
var safe = SafePath(mediaRoot, name);
if (safe == null) { await ctx.Status(403); return; }
await ctx.ServeFileAsync(safe);
});
static string SafePath(string baseDir, string userInput)
{
var combined = Path.GetFullPath(Path.Combine(baseDir, userInput));
var baseFull = Path.GetFullPath(baseDir);
if (!combined.StartsWith(baseFull + Path.DirectorySeparatorChar,
System.StringComparison.Ordinal)
&& combined != baseFull)
return null;
return combined;
}What ServeFileAsync handles for you:
Content-Typeinferred from extension (.mp4→video/mp4, etc.); override viacontentType:argumentContent-Length,Accept-Ranges: bytes,Last-Modifiedset automaticallyRange: bytes=N-M/bytes=N-/bytes=-N→ 206 Partial Content withContent-Range- Unsatisfiable range → 416 with
Content-Range: bytes */N - Malformed
Rangesyntax → 400 - Missing file → 404
- Streams without buffering the whole file in memory — fine for multi-GB videos
Path sanitization is your responsibility — the snippet above shows the typical pattern.
server.MapGet("/download/{name}", async ctx =>
{
var name = ctx.RouteValues["name"];
var safe = SafePath(mediaRoot, name);
if (safe == null) { await ctx.Status(403); return; }
// RFC 5987-encoded filename handles Korean / non-ASCII safely.
var encoded = System.Uri.EscapeDataString(Path.GetFileName(safe));
ctx.SetHeader("Content-Disposition",
$"attachment; filename=\"download\"; filename*=UTF-8''{encoded}");
await ctx.ServeFileAsync(safe);
});OPTIONS requests are auto-answered with 200 and:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type
For more restrictive CORS, add the headers you want from inside your Map* handlers via ctx.SetHeader(...). Tunable preflight behavior is on the v0.1 roadmap.
| Platform | Status | Notes |
|---|---|---|
| Windows (Mono, Editor) | ✅ Verified | Used during development |
| Windows (IL2CPP standalone) | ✅ Verified | File serving + video Range seeking confirmed in a build |
| Linux desktop | Standard HttpListener — expected to work |
|
| macOS | Standard HttpListener — expected to work |
|
| Android (IL2CPP) | Should work; INTERNET permission auto-added by Unity |
|
| iOS | Should work; may be killed in background | |
| WebGL | ❌ Not supported | Browser has no socket API |
| Consoles (Switch / PlayStation / Xbox) | ❌ Not supported | Platform networking SDKs required |
Minimum Unity: 2022.3.
The default binding http://*:{port}/ may require a one-time URL ACL on Windows for non-admin processes:
netsh http add urlacl url=http://*:8080/ user=EveryoneA configurable BindAddress (localhost / specific interface) is planned for v0.1.
Chromium-based browsers (Chrome, Edge, Brave, Opera) refuse to connect to certain "well-known service" ports with ERR_UNSAFE_PORT. Notable blocked ports include 25 (SMTP), 139 (NetBIOS), 6000 (X11), 6667 (IRC), 10080 (AMANDA backup), and many others.
The default of 8080 is safe. Other browser-friendly choices: 8000, 8081, 8888, 3000, 5000, 10081.
Public types: UniAPIServer, UniAPIContext.
UniAPIServer:
Map(method, pattern, handler)/MapGet/MapPost/MapPut/MapDelete/MapPatch— chainableStartServer()/StopServer()Port,IsRunning,RouteCount
UniAPIContext:
- Response:
Text(body, statusCode, contentType),Bytes(data, ...),Status(int),NotFound(),SetHeader(name, value),ServeFileAsync(path, contentType=null) - Request body:
ReadBodyAsTextAsync(),ReadBodyAsBytesAsync() - Main thread dispatch:
MainThreadAsync(Action),MainThreadAsync<T>(Func<T>),MainThreadAndWait(Action, timeoutMs) - Properties:
Request,Response,RouteValues,CancellationToken,Method,Path,IsResponseSent
- Wildcard routes (
{*rest}) and constraint converters ({id:int}) - Specificity-based matching (literal beats parameter automatically)
BindAddressproperty — localhost-only or specific interface- Configurable / opt-out auto-OPTIONS handling
- Automatic
HEADfor registeredGETroutes - JSON helpers (
ctx.Json(obj)) with pluggable serializer Content-Dispositionshortcut onServeFileAsync- Conditional GET (
If-Modified-Since,ETag) - Verified IL2CPP standalone + Android builds
This package is developed and tracked on Codeberg. Bug reports, feature requests, and questions go there:
- Issue tracker: codeberg.org/degubites/UniAPI/issues
Pull requests are welcome — please open an issue first for anything non-trivial so we can align on the approach before you spend time on a patch.
MIT — see LICENSE.md.
