reckfellENGINE

HTTP: получить JSON, проверить ответ и отменить запрос

Готовый C#-клиент и локальный сервер: GET, POST, ошибки транспорта/HTTP/JSON, лимиты и отмена.

На этой странице

Запустить рабочий запрос

Нужен C#-проект Reckfell 1.0.0. Для локального стенда нужен Python 3 без дополнительных пакетов; если у вас уже есть API, используйте его URL и ответ вида {"message":"текст"}.

  1. Сохраните LocalHttpFixture.py в отдельной папке и выполните в ней python LocalHttpFixture.py. Оставьте терминал открытым. Сервер слушает только 127.0.0.1:8765; это стенд, а не сервер для публикации игры.
  2. Добавьте HttpJsonRequest.cs и HttpJsonDemo.cs в проект. Создайте отдельный уровень с корнем Entity, прикрепите HttpJsonDemo к корню, соберите C# и нажмите F6.
  3. В Output появятся transport=Success, status=200 и message=Привет 🌍. Если порт занят, остановите свой предыдущий стенд или выберите другой порт в create_server и поле Url.
HttpJsonDemo.cs
C#
using Reckfell;

public partial class HttpJsonDemo : Entity
{
    [Export] public string Url { get; set; } = "http://127.0.0.1:8765/message";
    public override void _Ready()
    {
        var api = new HttpJsonRequest(); AddChild(api);
        api.Completed += reply => ReckfellApi.Print($"HTTP: transport={reply.Transport}, status={reply.StatusCode}, failure={reply.Failure}, message={reply.Message}");
        Error start = api.Start(Url);
        if (start != Error.Ok) ReckfellApi.Print($"HTTP start: {start}");
    }
}
HttpJsonRequest.cs
C#
using System;
using System.Text.Json;
using Reckfell;

public partial class HttpJsonRequest : Entity
{
    public sealed record Reply(HttpRequest.Result Transport, int StatusCode, string Message, string Failure)
    {
        public bool IsSuccess => Failure.Length == 0;
    }
    public int MaxBytes { get; set; } = 65536;
    public double TimeoutSeconds { get; set; } = 5;
    public bool UseThreads { get; set; }
    public TlsOptions? Trust { get; set; }
    public bool Busy => _active is not null;
    public Reply? LastReply { get; private set; }
    public event Action<Reply>? Completed;
    private HttpRequest? _active;

    public Error Start(string url, HttpClient.Method method = HttpClient.Method.Get, string jsonBody = "")
    {
        if (Busy) return Error.Busy;
        if (!IsInsideTree()) return Error.Unconfigured;
        if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) ||
            (uri.Scheme != "http" && uri.Scheme != "https") || uri.UserInfo.Length != 0 ||
            MaxBytes <= 0 || !double.IsFinite(TimeoutSeconds) || TimeoutSeconds <= 0)
            return Error.InvalidParameter;
        var request = new HttpRequest { BodySizeLimit = MaxBytes, Timeout = TimeoutSeconds, MaxRedirects = 0, UseThreads = UseThreads };
        if (Trust is not null) request.SetTlsOptions(Trust);
        _active = request; LastReply = null; AddChild(request);
        request.RequestCompleted += (result, code, headers, body) =>
        {
            if (_active != request) return;
            _active = null;
            var transport = (HttpRequest.Result)result;
            Reply reply;
            if (transport != HttpRequest.Result.Success) reply = new(transport, (int)code, "", "transport");
            else if (code < 200 || code >= 300) reply = new(transport, (int)code, "", "http");
            else
            {
                try
                {
                    using JsonDocument doc = JsonDocument.Parse(body);
                    if (doc.RootElement.ValueKind == JsonValueKind.Object &&
                        doc.RootElement.TryGetProperty("message", out JsonElement value) &&
                        value.ValueKind == JsonValueKind.String && value.GetString() is string message && message.Length <= 128)
                        reply = new(transport, (int)code, message, "");
                    else reply = new(transport, (int)code, "", "schema");
                }
                catch (JsonException) { reply = new(transport, (int)code, "", "json"); }
            }
            LastReply = reply; request.QueueFree(); Completed?.Invoke(reply);
        };
        string[] headers = jsonBody.Length == 0 ? ["Accept: application/json"] :
            ["Accept: application/json", "Content-Type: application/json; charset=utf-8"];
        Error error = request.Request(url, headers, method, jsonBody);
        if (error != Error.Ok) { _active = null; request.CancelRequest(); request.QueueFree(); }
        return error;
    }
    public void Cancel()
    {
        HttpRequest? request = _active; _active = null;
        if (request is null) return;
        request.CancelRequest(); request.QueueFree();
    }
    public override void _ExitTree() => Cancel();
}
LocalHttpFixture.py
Python
"""A loopback-only HTTP fixture. Python 3 standard library; no production data."""
import gzip
import json
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    protocol_version = 'HTTP/1.1'

    def log_message(self, *args):
        pass

    def handle(self):
        try:
            super().handle()
        except ConnectionError:
            self.close_connection = True

    def reply(self, status, body, headers=None):
        self.send_response(status)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', str(len(body)))
        for name, value in (headers or {}).items():
            self.send_header(name, value)
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        body = json.dumps({'message': 'Привет 🌍'}, ensure_ascii=False).encode('utf-8')
        path = self.path.split('?', 1)[0]
        try:
            if path == '/slow':
                time.sleep(0.5)
            if path == '/missing':
                self.reply(404, b'{"error":"missing"}')
            elif path == '/bad-json':
                self.reply(200, b'{')
            elif path == '/wrong-schema':
                self.reply(200, b'{"message":42}')
            elif path == '/empty':
                self.reply(204, b'')
            elif path == '/large':
                self.reply(200, b'x' * 70000)
            elif path == '/gzip':
                self.reply(200, gzip.compress(body), {'Content-Encoding': 'gzip'})
            elif path == '/gzip-large':
                self.reply(200, gzip.compress(b'x' * 70000), {'Content-Encoding': 'gzip'})
            elif path == '/redirect':
                self.reply(302, b'', {'Location': '/message'})
            elif path in ('/chunked', '/eof', '/truncated'):
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                if path == '/chunked':
                    self.send_header('Transfer-Encoding', 'chunked')
                elif path == '/truncated':
                    self.send_header('Content-Length', str(len(body) + 20))
                else:
                    self.send_header('Connection', 'close')
                self.end_headers()
                if path == '/chunked':
                    for start in range(0, len(body), 4):
                        chunk = body[start:start + 4]
                        self.wfile.write(f'{len(chunk):X}\r\n'.encode() + chunk + b'\r\n')
                    self.wfile.write(b'0\r\n\r\n')
                else:
                    self.wfile.write(body)
                    self.close_connection = True
            else:
                self.reply(200, body)
        except ConnectionError:
            self.close_connection = True

    def do_POST(self):
        length = int(self.headers.get('Content-Length', '0'))
        if length > 65536:
            self.reply(413, b'{}')
            self.close_connection = True
            return
        self.reply(200, self.rfile.read(length))


def create_server(port=8765):
    server = ThreadingHTTPServer(('127.0.0.1', port), Handler)
    server.daemon_threads = True
    return server


if __name__ == '__main__':
    with create_server() as server:
        print('HTTP fixture: http://127.0.0.1:8765/message', flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            pass

Три проверки результата

Start возвращает Error запуска. Error.Ok означает, что запрос принят, а не что сервер уже ответил. Completed приходит позднее и содержит три независимых уровня проверки:

ПроверкаЧто означает ошибка
Transport != SuccessСоединение, TLS, тайм-аут, лимит или передача не завершились успешно
StatusCode вне 200–299Сервер ответил, но запрос не принят приложением; 404 может прийти с Transport = Success
Failure = json или schemaБайты получены, но JSON повреждён либо нет строкового message длиной до 128 символов

Этот клиент рассчитан на JSON-объект с message. Ответ 204 без тела допустим в HTTP, но не соответствует этому формату. Для другого API задайте его схему и правила статусов; не превращайте любой ответ в успешное игровое действие.

В Inspector меняйте только путь полного Url, например http://127.0.0.1:8765/missing: /missing → http/404; /bad-json → json; /wrong-schema → schema; /large и /gzip-large → BodySizeLimitExceeded; /chunked и /gzip → правильное сообщение. Для /slow в HttpJsonDemo замените создание клиента на new HttpJsonRequest { TimeoutSeconds = 0.1 }. Многобайтовые символы проверяются после получения полного тела, а не декодированием каждого сетевого фрагмента отдельно.

POST и двоичные данные

Start принимает метод и jsonBody. Для отправки сообщения вызовите Start(url, HttpClient.Method.Post, jsonBody) из своего обработчика; JSON создавайте сериализатором, а не склейкой строк с пользовательским вводом. Заголовок Content-Type добавляется, когда jsonBody не пуст. Локальный /echo возвращает полученные байты и позволяет проверить POST.

Строковый Request кодирует тело в UTF-8. Для изображения/архива/другого двоичного формата используйте HttpRequest.RequestRaw, byte[] и соответствующий Content-Type; JSON-клиент выше для такого ответа не подходит. При неоднозначности перегрузок string[]/ReadOnlySpan задайте массив заголовков явно.

Отмена, повтор и время жизни

Один экземпляр клиента обслуживает один запрос; повторный Start во время Busy возвращает Error.Busy. Для параллельных независимых запросов создайте отдельные экземпляры. Cancel снимает Busy и освобождает текущий HttpRequest. Сигнал Completed при отмене не обещан: в этом коде он не вызывается. Если интерфейс показывает отмену, обновите его из обработчика отмены.

Каждый запуск создаёт отдельный HttpRequest. Проверка _active != request отбрасывает старый ответ после отмены и замены. _ExitTree отменяет запрос при удалении владельца. Не оставляйте обработчик, который обращается к уже удалённому экрану.

Не запускайте повтор в каждом _Process. Ограничьте число попыток и задержку; повтор POST может повторить операцию сервера. Повтор выполнения должен быть безопасен по правилам вашего API.

Лимиты, перенаправления и файлы

Здесь лимит тела 65536 байт, TimeoutSeconds = 5, MaxRedirects = 0. HttpRequest автоматически распаковывает gzip/deflate при AcceptGZip; лимит проверяется и для распакованного результата. Тайм-аут использует таймер с IgnoreTimeScale, однако обработка зависит от работы дерева и его режима паузы. Не считайте его независимым процессом вне EntityTree.

MaxRedirects = 0 намеренно запрещает автоматический переход: /redirect заканчивается RedirectLimitReached. Если API перенаправляет запросы, отдельно проверьте допустимые адреса, метод и заголовки. Не переносите авторизацию на произвольный сервер.

UseThreads включает внутренний рабочий поток, если он доступен в сборке. Он не переносит ваш JSON-разбор и UI-обработчики в произвольный безопасный фон. Для обычного запроса ручной Poll не нужен; низкоуровневый HttpClient работает иначе.

DownloadFile пишет тело в файл, а байты сигнала остаются пустыми. Даже тело ответа 404 может оказаться в этом файле. Скачивайте в отдельный временный путь, проверяйте результат транспорта, HTTP-статус и ожидаемое содержимое/хеш, затем публикуйте файл. При отмене учитывайте KeepPartialDownload; AppendToDownloadFile — не готовая система корректного возобновления без проверки ответа сервера.

Проверены настоящие локальные TCP-запросы, UTF-8, GET/POST, двоичные байты, gzip/chunked/EOF, повреждённые ответы, лимиты, тайм-аут, отмена/замена и удаление владельца. Для HTTPS используйте проверку сертификата. Экспорты, прокси, браузерные ограничения и мобильные разрешения требуют отдельной проверки.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.