HEAD auf den Download, und /api verschluckt nichts mehr
Zwei Fehler, derselbe Grund — und beide zeigte erst der Browser.
**Der Spieler bekam die Webseite statt des Films.** „MEDIA_ELEMENT_ERROR:
Format error", 531 Byte, `Content-Type: text/html`. Das ist genau die Grösse
von `index.html`.
`MapGet` nimmt HEAD nicht an. Die Oberfläche fragt aber mit HEAD nach, ob das
Umpacken fertig ist — einen halbfertigen Film herunterzuladen, nur um das zu
erfahren, wäre absurd. Die Anfrage fiel durch den Methodenabgleich hindurch
bis zum SPA-Rückfall, und der nimmt jeden Pfad ohne Dateiendung. Antwort:
200, mit der Startseite im Rumpf. Der Client hielt das Umpacken für fertig
und reichte HTML an `<video>` weiter.
Jetzt `MapMethods` mit GET und HEAD. Kestrel lässt den Rumpf bei HEAD selbst
weg.
**Und ein vertippter Aufruf sah aus wie ein Erfolg.** `GET /api/v1/gibtsnicht`
gab 200 und die Webseite zurück — derselbe Rückfall. Ein Fehler, der erst
dort auffällt, wo jemand HTML durch einen JSON-Parser schickt.
`app.Map("/api/{*rest}", 404)` davor. Buchstaben schlagen Platzhalter, also
gewinnt jeder echte Endpunkt; was übrig bleibt, ist nicht da und sagt das
auch.
350 Tests grün, die zwei neuen eingerechnet. Am Container nachgemessen: HEAD
auf den Download gibt 200 `video/mp4` mit 71 920 Byte, `/api/v1/gibtsnicht`
gibt 404, und ein Neuladen auf `/verwaltung` bekommt weiterhin die Seite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c4081ff251
commit
67cf0fd1d9
@@ -18,7 +18,13 @@ public static class DeliveryEndpoints
|
|||||||
return Results.Ok(Auskunft(plan, packer, datei.Id, datei.ModifiedAt));
|
return Results.Ok(Auskunft(plan, packer, datei.Id, datei.ModifiedAt));
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapGet("/api/v1/files/{id:guid}/download", async (
|
// **GET und HEAD.** Der Client fragt mit HEAD nach, ob das Umpacken
|
||||||
|
// fertig ist — eine Antwort ohne Rumpf genügt dafür, und einen
|
||||||
|
// halbfertigen Film herunterzuladen, nur um das zu erfahren, wäre
|
||||||
|
// absurd. `MapGet` allein nimmt HEAD nicht an; die Anfrage fiele dann
|
||||||
|
// durch bis zum SPA-Rückfall und bekäme die Webseite mit Status 200.
|
||||||
|
// Genau das ist passiert: der Spieler bekam HTML statt Video.
|
||||||
|
app.MapMethods("/api/v1/files/{id:guid}/download", ["GET", "HEAD"], async (
|
||||||
Guid id, FlaroContext db, PackageCoordinator packer) =>
|
Guid id, FlaroContext db, PackageCoordinator packer) =>
|
||||||
{
|
{
|
||||||
var gefunden = await Laden(db, id);
|
var gefunden = await Laden(db, id);
|
||||||
|
|||||||
@@ -140,8 +140,15 @@ app.MapDelivery();
|
|||||||
app.MapPlayback();
|
app.MapPlayback();
|
||||||
app.MapMatching();
|
app.MapMatching();
|
||||||
|
|
||||||
// Alles, was keine Schnittstelle ist, bekommt die Seite — der Router läuft
|
// **Was unter /api nicht beantwortet wurde, ist nicht da.** Ohne diese Zeile
|
||||||
// im Browser. Ohne das endet ein Neuladen auf /admin in einem 404.
|
// fängt der Rückfall darunter jeden unbekannten Pfad ab und liefert die
|
||||||
|
// Webseite mit Status 200 — ein vertippter Aufruf sieht dann aus wie ein
|
||||||
|
// Erfolg, und der Fehler fällt erst dort auf, wo HTML verarbeitet wird.
|
||||||
|
// Buchstaben schlagen Platzhalter, also gewinnt jeder echte Endpunkt hier.
|
||||||
|
app.Map("/api/{*rest}", () => Results.NotFound()).AllowAnonymous();
|
||||||
|
|
||||||
|
// Alles Übrige bekommt die Seite — der Router läuft im Browser. Ohne das
|
||||||
|
// endet ein Neuladen auf /verwaltung in einem 404.
|
||||||
if (Directory.Exists(oberfläche))
|
if (Directory.Exists(oberfläche))
|
||||||
{
|
{
|
||||||
app.MapFallback(() => Results.File(Path.Combine(oberfläche, "index.html"), "text/html"))
|
app.MapFallback(() => Results.File(Path.Combine(oberfläche, "index.html"), "text/html"))
|
||||||
|
|||||||
@@ -180,6 +180,30 @@ public class DeliveryTests
|
|||||||
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Der Client fragt mit HEAD nach, ob das Umpacken fertig ist.</summary>
|
||||||
|
///
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Der Fehler, den erst der Browser zeigte.</b> <c>MapGet</c> nimmt
|
||||||
|
/// HEAD nicht an. Die Anfrage fiel durch bis zum SPA-Rückfall, bekam
|
||||||
|
/// <c>200 text/html</c>, und der Spieler hielt die Webseite für den Film:
|
||||||
|
/// „MEDIA_ELEMENT_ERROR: Format error", 531 Byte lang.
|
||||||
|
/// </remarks>
|
||||||
|
[Test]
|
||||||
|
public async Task HEAD_auf_den_Download_antwortet_mit_dem_Download()
|
||||||
|
{
|
||||||
|
Film("Kopfpruefung (2021)", "mp4");
|
||||||
|
var (client, datei) = await Eingelesen();
|
||||||
|
|
||||||
|
var antwort = await client.SendAsync(
|
||||||
|
new HttpRequestMessage(HttpMethod.Head, $"/api/v1/files/{datei.Id}/download"));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
|
Assert.That(antwort.Content.Headers.ContentType?.MediaType, Is.EqualTo("video/mp4"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Ohne_Anmeldung_gibt_es_nichts()
|
public async Task Ohne_Anmeldung_gibt_es_nichts()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -140,6 +140,38 @@ public sealed class WebInterfaceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ein unbekannter Pfad unter /api ist ein Fehler, keine Seite.</summary>
|
||||||
|
///
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Der Fund, der dahintersteckt.</b> Der Rückfall für den Router im
|
||||||
|
/// Browser nimmt jeden Pfad ohne Dateiendung — auch
|
||||||
|
/// <c>/api/v1/vertippt</c>. Ein vertippter Aufruf bekam die Webseite mit
|
||||||
|
/// Status 200 zurück und sah damit wie ein Erfolg aus.
|
||||||
|
/// </remarks>
|
||||||
|
[Test]
|
||||||
|
public async Task Ein_unbekannter_Schnittstellenpfad_gibt_404()
|
||||||
|
{
|
||||||
|
var wurzel = Path.Combine(Path.GetTempPath(), "flaro-www-" + Guid.CreateVersion7().ToString("N"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(wurzel, "wwwroot"));
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(wurzel, "wwwroot", "index.html"), "<!doctype html><title>Flaro</title>");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var server = new FlaroServer(inhaltswurzel: wurzel);
|
||||||
|
var antwort = await server.OhneGedaechtnis().GetAsync("/api/v1/gibtsnicht");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||||||
|
Assert.That(antwort.Content.Headers.ContentType?.MediaType, Is.Not.EqualTo("text/html"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { Directory.Delete(wurzel, recursive: true); } catch (IOException) { /* Aufräumen ist Kür */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Nimmt den Namen und den Wert aus der Set-Cookie-Kopfzeile.</summary>
|
/// <summary>Nimmt den Namen und den Wert aus der Set-Cookie-Kopfzeile.</summary>
|
||||||
private static string Keks(HttpResponseMessage antwort) =>
|
private static string Keks(HttpResponseMessage antwort) =>
|
||||||
antwort.Headers.GetValues("Set-Cookie").Single().Split(';')[0];
|
antwort.Headers.GetValues("Set-Cookie").Single().Split(';')[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user