Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement RequestError #119

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open

feat: implement RequestError #119

wants to merge 2 commits into from

Conversation

janbritz
Copy link
Contributor

Mithilfe einer Middleware werden alle Exceptions zu dem in der QPPE definierten RequestError transformiert.
Sollte eine unbekannte Exception geraised werden, wird diese zu einem ServerError umgewandelt.

Außerdem wird die veraltete NotFoundStatus-Komponente aus der QPPE entfernt.

PS: da ich Probleme mit pytest hatte, musste ich poe up ausführen, das Update kann ich aber gerne auch aus dem PR entfernen.

Copy link
Contributor

@MartinGauk MartinGauk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die Updates der Abhängigkeiten sollten nicht mehr notwendig sein. Zur besseren Übersicht sollten wir auf Updates in PRs verzichten, wenn es nicht inhaltlich notwendig ist.

except web.HTTPException:
raise
except tuple(exception_map.keys()) as e:
exception = exception_map[e.__class__]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hier besser type(e) benutzen

exception = exception_map[e.__class__]
raise exception(reason=e.reason, temporary=e.temporary) from e
except Exception as e:
raise ServerError(reason=e.__class__.__name__, temporary=True) from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bei unbekannten Exceptions sollten wir nach Außen hin keine Details geben, aber es sollte geloggt werden, inkl. Traceback.

questionpy_server/worker/impl/subprocess.py Show resolved Hide resolved
from questionpy_server.worker.runtime.messages import WorkerMemoryLimitExceededError, WorkerUnknownError

exception_map: dict[type[QPyBaseError], type[QpyWebError]] = {
InvalidQuestionStateError: InvalidPackageError,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das ist eher ein PackageError.

exception_map: dict[type[QPyBaseError], type[QpyWebError]] = {
InvalidQuestionStateError: InvalidPackageError,
StaticFileSizeMismatchError: InvalidPackageError,
WorkerNotRunningError: InvalidPackageError,
Copy link
Contributor

@MartinGauk MartinGauk Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkerNotRunningError deutet eher auf einen Programmierfehler hin. Sollte daher in diese map nicht aufgenommen werden.

questionpy_common/error.py Show resolved Hide resolved
questionpy_server/web/errors.py Show resolved Hide resolved
Comment on lines +55 to +56
except web.HTTPException:
raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich denke es wäre wünschenswert, dass alle generierten HTTP-Fehler einen Body der gleichen Struktur haben. Wenn ich das richtig sehe, werden noch an 4 Stellen direkt HTTPExceptions ohne Body geworfen. Ich würde gerne entweder diese Stellen durch QPyBaseErrors ersetzen, oder ihnen hier einen Body verpassen, wenn sie noch keinen haben.

questionpy_server/web/errors.py Show resolved Hide resolved
Comment on lines +64 to +73
class InvalidRequestError(web.HTTPBadRequest, _ExceptionMixin):
def __init__(self, *, reason: str | None, temporary: bool) -> None:
super().__init__(
"Invalid request body was provided",
RequestError(
error_code=RequestErrorCode.INVALID_REQUEST,
reson=reason,
temporary=temporary,
),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Die Meinungen gehen hier auseinander, aber in Feat/validation errors questionpy-sdk#126 hat @tumidi zuletzt 422 Unprocessable Entity verwendet, was für einen semantischen Fehler (in Abgrenzung zu einem syntaktischen Fehler wie kaputtes JSON) steht. Ich habe da selbst keine starke Meinung zu, außer, dass wir konsistent sein sollten.

  2. Ich bin mir generell der Sinnhaftigkeit der temporary-Flag nicht so ganz sicher, zumindest sollte es aber an dieser Stelle kein Konstruktor-Argument sein. Die meisten HTTP-Status-Codes implizieren, ob der Fehler bei gleichem Request erneut zu erwarten ist. So steht bei 400 z.B.

    The client SHOULD NOT repeat the request without modifications.

    D.h. ein 400er muss eigentlich immer temporary=False sein, daher sollte das auch fest für diese Klasse so sein. Das gilt analog vermutlich für einige andere Fehler auch, aber dieser ist ein gutes Beispiel.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja, da hast du genau genommen recht.

Der Sinn hinter dem Flag und warum ich den gerne in allen Fehlerfällen ausgegeben haben will, ist dass das LMS nur an einer Stelle gucken muss, ob es sich um einen temporären oder permanenten Fehler handelt. Wir sind dann in der Lage, neue Error-Codes zu definieren, die das LMS noch nicht kennt, und trotzdem weiß das LMS, ob es die Anfrage später nochmal stellen sollte.

Comment on lines +94 to +102
class RequestErrorCode(Enum):
QUEUE_WAITING_TIMEOUT = "QUEUE_WAITING_TIMEOUT"
WORKER_TIMEOUT = "WORKER_TIMEOUT"
OUT_OF_MEMORY = "OUT_OF_MEMORY"
INVALID_PACKAGE = "INVALID_PACKAGE"
INVALID_REQUEST = "INVALID_REQUEST"
PACKAGE_ERROR = "PACKAGE_ERROR"
CALLBACK_API_ERROR = "CALLBACK_API_ERROR"
SERVER_ERROR = "SERVER_ERROR"
Copy link
Member

@MHajoha MHajoha Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Ich würde mir mehr Trennschärfe wünschen. Zumindest folgende Fälle halte ich für sinnvoll, zu unterscheiden, damit wir passende Fehlermeldungen anzeigen können:
    • Paket existiert nicht
    • Options invalide
    • Question state kaputt
    • Attempt state kaputt
  2. Hier ginge auch auto() für weniger duplication.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Wenn wir in allen Fehlern (status code != 2xx) ein gleichartiges JSON-Objekt zurückgeben, was ich begrüße, ist der Fall "Paket existiert nicht" natürlich sinnvoll. Die anderen drei genannten Fälle fallen eigentlich unter den generischen PACKAGE_ERROR. Wir können hier aber auch differenzieren. InvalidQuestionStateError ist zumindest schon definiert in common, wird benutzt und kann dann leicht als Fehlerfall kommuniziert werden.
    Welchen Fehlerfall meinst du bei "Options invalide"?

  2. "Using auto with StrEnum results in the lower-cased member name as the value."
    ist daher in diesem Fall nicht so gut
    Generell würde ich auto() nicht im Zusammenhang mit Schnittstellen benutzen, da sich das Verhalten möglicherweise in neueren Versionen ändern kann.

Copy link
Member

@MHajoha MHajoha Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mit "Options invalide" meine ich OptionsFormValidationError, wobei das tbf auch auf #118 warten musste.

Ich denke, "Question state kaputt" und "Attempt state kaputt" wäre insofern hilfreich zu unterscheiden, dass wir dann in der Fehlermeldung erkennbar machen können, dass entweder die ganze Frage oder "nur" der Attempt fehlerhaft ist. Das hilft zwar keiner Studi, aber womöglich schon Lehrenden (und Entwickler:innen eh)

Zu 2.: Pardon, ich war davon ausgegangen, dass auto() den Namen verbatim nimmt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bezüglich OptionsFormValidationError kommen wir an einen guten Punkt, den wir aber nicht hier angehen sollten. Ich habe dafür Issue #122 erstellt.

Ich denke, "Question state kaputt" und "Attempt state kaputt" wäre insofern hilfreich zu unterscheiden, dass wir dann in der Fehlermeldung erkennbar machen können, dass entweder die ganze Frage oder "nur" der Attempt fehlerhaft ist. Das hilft zwar keiner Studi, aber womöglich schon Lehrenden (und Entwickler:innen eh)

Dem schließe ich mich an. @janbritz Bitte führe in common noch eine neue Exception InvalidAttemptStateError ein und ergänze zwei neue Error Codes INVALID_QUESTION_STATE und INVALID_ATTEMPT_STATE.

Comment on lines +76 to +85
class PackageError(web.HTTPBadRequest, _ExceptionMixin):
def __init__(self, *, reason: str | None, temporary: bool) -> None:
super().__init__(
"An error occurred within the package",
RequestError(
error_code=RequestErrorCode.PACKAGE_ERROR,
temporary=temporary,
reason=reason,
),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aus Sicht des Clients ist das vermutlich auch eher ein Internal Server Error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stimmt, ebenso WorkerTimeoutError und OutOfMemoryError. Bei InvalidPackageError hinge es davon ab, ob das Paket durch das LMS bereitgestellt wurde oder vom Server angeboten wurde, aber wir sollten das nicht zu akademisch betrachten und auch hier 500 zurückgeben.

questionpy_server/worker/runtime/messages.py Show resolved Hide resolved
questionpy_server/web/_decorators.py Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants