[backend/masto-client] Handle idempotency key header (ISH-75)
This commit is contained in:
parent
bbf1afce5b
commit
07edffa6b5
3 changed files with 47 additions and 3 deletions
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
|
@ -19,7 +20,12 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class StatusController(DatabaseContext db, NoteRenderer noteRenderer, NoteService noteSvc) : Controller {
|
public class StatusController(
|
||||||
|
DatabaseContext db,
|
||||||
|
NoteRenderer noteRenderer,
|
||||||
|
NoteService noteSvc,
|
||||||
|
IDistributedCache cache
|
||||||
|
) : Controller {
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authenticate("read:statuses")]
|
[Authenticate("read:statuses")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
|
@ -119,7 +125,25 @@ public class StatusController(DatabaseContext db, NoteRenderer noteRenderer, Not
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
|
||||||
//TODO: handle scheduled statuses
|
//TODO: handle scheduled statuses
|
||||||
//TODO: handle Idempotency-Key
|
Request.Headers.TryGetValue("Idempotency-Key", out var idempotencyKeyHeader);
|
||||||
|
var idempotencyKey = idempotencyKeyHeader.FirstOrDefault();
|
||||||
|
if (idempotencyKey != null) {
|
||||||
|
var hit = await cache.FetchAsync($"idempotency:{idempotencyKey}", TimeSpan.FromHours(24),
|
||||||
|
() => $"_:{HttpContext.TraceIdentifier}");
|
||||||
|
|
||||||
|
if (hit != $"_:{HttpContext.TraceIdentifier}") {
|
||||||
|
for (var i = 0; i <= 10; i++) {
|
||||||
|
if (!hit.StartsWith('_')) break;
|
||||||
|
await Task.Delay(100);
|
||||||
|
hit = await cache.GetAsync<string>($"idempotency:{idempotencyKey}")
|
||||||
|
?? throw new Exception("Idempotency key status disappeared in for loop");
|
||||||
|
if (i >= 10)
|
||||||
|
throw GracefulException.RequestTimeout("Failed to resolve idempotency key note within 1000 ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetNote(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Text == null && request.MediaIds is not { Count: > 0 } && request.Poll == null)
|
if (request.Text == null && request.MediaIds is not { Count: > 0 } && request.Poll == null)
|
||||||
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
||||||
|
@ -139,6 +163,10 @@ public class StatusController(DatabaseContext db, NoteRenderer noteRenderer, Not
|
||||||
|
|
||||||
var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply,
|
var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply,
|
||||||
attachments: attachments);
|
attachments: attachments);
|
||||||
|
|
||||||
|
if (idempotencyKey != null)
|
||||||
|
await cache.SetAsync($"idempotency:{idempotencyKey}", note.Id, TimeSpan.FromHours(24));
|
||||||
|
|
||||||
var res = await noteRenderer.RenderAsync(note, user);
|
var res = await noteRenderer.RenderAsync(note, user);
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
|
|
|
@ -49,6 +49,12 @@ public static class DistributedCacheExtensions {
|
||||||
return fetched;
|
return fetched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<T> FetchAsync<T>(
|
||||||
|
this IDistributedCache cache, string key, TimeSpan ttl, Func<T> fetcher
|
||||||
|
) where T : class {
|
||||||
|
return await FetchAsync(cache, key, ttl, () => Task.FromResult(fetcher()));
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<T> FetchAsyncValue<T>(
|
public static async Task<T> FetchAsyncValue<T>(
|
||||||
this IDistributedCache cache, string key, TimeSpan ttl, Func<Task<T>> fetcher
|
this IDistributedCache cache, string key, TimeSpan ttl, Func<Task<T>> fetcher
|
||||||
) where T : struct {
|
) where T : struct {
|
||||||
|
@ -60,6 +66,12 @@ public static class DistributedCacheExtensions {
|
||||||
return fetched;
|
return fetched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<T> FetchAsyncValue<T>(
|
||||||
|
this IDistributedCache cache, string key, TimeSpan ttl, Func<T> fetcher
|
||||||
|
) where T : struct {
|
||||||
|
return await FetchAsyncValue(cache, key, ttl, () => Task.FromResult(fetcher()));
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task SetAsync<T>(this IDistributedCache cache, string key, T data, TimeSpan ttl) {
|
public static async Task SetAsync<T>(this IDistributedCache cache, string key, T data, TimeSpan ttl) {
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
await JsonSerializer.SerializeAsync(stream, data);
|
await JsonSerializer.SerializeAsync(stream, data);
|
||||||
|
|
|
@ -126,6 +126,10 @@ public class GracefulException(
|
||||||
return new GracefulException(HttpStatusCode.BadRequest, message, details);
|
return new GracefulException(HttpStatusCode.BadRequest, message, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GracefulException RequestTimeout(string message, string? details = null) {
|
||||||
|
return new GracefulException(HttpStatusCode.RequestTimeout, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
public static GracefulException RecordNotFound() {
|
public static GracefulException RecordNotFound() {
|
||||||
return new GracefulException(HttpStatusCode.NotFound, "Record not found");
|
return new GracefulException(HttpStatusCode.NotFound, "Record not found");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue