[HN Gopher] Hyrum's Law in Golang
___________________________________________________________________
Hyrum's Law in Golang
Author : thunderbong
Score : 244 points
Date : 2024-11-21 07:12 UTC (15 hours ago)
(HTM) web link (abenezer.org)
(TXT) w3m dump (abenezer.org)
| sudhirj wrote:
| Weren't there a couple of anecdotes where Windows couldn't fix a
| bug because some popular game (maybe SimCity?) depended on it, so
| the devs hardcoded a SimCity check inside Windows and made the
| bug happen if it was running?
| masklinn wrote:
| It was not a bug in windows, it was a bug in SimCity: it would
| UAF some memory, but the Windows 3.x allocator did not unmap /
| clear that memory so it worked.
|
| Windows 95 changed that, and so one of the compatibility shims
| it got is that the allocator had a 3.x adjacent mode, which
| would be turned on when running SimCity (and probably other
| similarly misbehaving software as well).
|
| Nowadays this is formalised in the _compatibility engine_
| (dating back to windows do), which can enable special modes or
| compatibility shims for applications (windows admins trying to
| run legacy or unmaintained applications can manage the
| application of compatibility modes via the "compatibility
| administrator").
| praptak wrote:
| Still a pretty good example of having to support something
| which is definitely not part of the official spec.
| guappa wrote:
| Had it been open source, they could have just fixed the
| software instead
| masklinn wrote:
| Fixing the upstream would not have updated it on the
| millions of machines running it, which is what they
| wanted to not break.
| cesarb wrote:
| > Fixing the upstream would not have updated it on the
| millions of machines running it,
|
| It was a very different world back then. You couldn't
| even assume a dial-up connection.
|
| Nowadays, the software would have been automatically
| updated for 99% of the machines running it, whether they
| wanted that update or not.
| masklinn wrote:
| Hah. Debian will happily keep shipping libraries years
| out of date. Then complain that you're holding them back
| when they finally wake up and update sid to a bleeding
| edge release.
| adontz wrote:
| https://www.joelonsoftware.com/2000/05/24/strategy-letter-ii...
|
| Jon Ross, who wrote the original version of SimCity for Windows
| 3.x, told me that he accidentally left a bug in SimCity where
| he read memory that he had just freed. Yep. It worked fine on
| Windows 3.x, because the memory never went anywhere. Here's the
| amazing part: On beta versions of Windows 95, SimCity wasn't
| working in testing. Microsoft tracked down the bug and added
| specific code to Windows 95 that looks for SimCity. If it finds
| SimCity running, it runs the memory allocator in a special mode
| that doesn't free memory right away. That's the kind of
| obsession with backward compatibility that made people willing
| to upgrade to Windows 95.
| praptak wrote:
| Corollary: uptime is part of the defacto spec being relied on.
|
| One of the SRE practices is breaking your service on purpose to
| bring the actual service level closer to what is promised and
| supported.
| dangfault wrote:
| another one, you pay me below market rate and you get below
| market rate code
| Cthulhu_ wrote:
| As another commenter pointed out, this is to a point what Go
| does as well; for example, map iteration is randomised so no
| implementation will rely on insertion order.
| hambes wrote:
| Solution to the specifically mentioned problem: Don't use string-
| based errors, use sentinel errors [1].
|
| More generally: Don't produce code where consumers of your API
| are the least bit inclined to rely on non-technical strings.
| Instead use first-level language constructs like predefined error
| values, types or even constants that contain the non-technical
| string so that API consumers can compare the return value
| againnst the constant instead of hard-coding the contained string
| themselves.
|
| Hyrum's Law is definitely a thing, but its effects can be
| mitigated.
|
| [1]: https://thomas-guettler.de/go/wrapping-and-sentinel-errors
| Svip wrote:
| In your example, the onus is on the consumer not the provider.
| I could still be writing code that checks if `err.String() ==
| "no more tea available."`. I agree, I shouldn't do that, but
| nothing is preventing me from doing that. Additionally,
| errors.Is is a relatively recent addition to Go, so by the time
| people would check for errors like this, it was just easier to
| check the literal string. But as an API provider in Go, you
| cannot prevent your consumers from checking the return values
| of .String().
| hambes wrote:
| Unfortunately true. The Go maintainers might not agree with
| me on this, but I think in this case consumers have to learn
| the hard way. Go tries to always be backwards compatible, but
| I don't think that trying to be backwards compatible with
| incorrect usage is ever the right choice.
| LudwigNagasena wrote:
| So the people who decided to make a stringly type error
| with `errors.New("http: request body too large")` and make
| you suffer, now can remove a stringly typed error and make
| you suffer even more? What would the lesson be? What would
| consumers learn?
| hambes wrote:
| I don't understand your point. The lesson is "don't rely
| on magic strings, instead rely on exported and documented
| constants, otherwise your code might break".
| LudwigNagasena wrote:
| My point is that a few years ago there was no exported
| and document constant. The lesson should be "provide
| sensible tools, otherwise your consumers will have to
| rely on implementation details for the most basic
| expected stuff".
| stonemetal12 wrote:
| >My point is that a few years ago there was no exported
| and document constant.
|
| Then the feature didn't exist. Figuring out undocumented
| implementation details to "make it work" is asking for it
| to be broken in the future. So if you are unwilling or
| unable to support fixing it in the future then don't do
| that.
|
| If it is "the most basic expected stuff" then quite
| literally make the determination that it isn't ready for
| use. A lot of Go was and maybe still be half baked and
| not ready for production. It is ok to recognize that and
| not use it.
| Joker_vD wrote:
| I am glad that your circumstances are such that you can
| just stop working on a project when the tooling it uses
| turns out to be inadequate, wait five years, and then
| come back when it improves.
|
| Unfortunately, many people can't really do that: when the
| ecosystem turns out to be somewhat inadequate in a
| project that's already been in use for couple of years,
| their options are either "just make it work one way or
| another, who cares if it's a hardcoded string, we have to
| ship the fix ASAP" or "rewrite it all in Rust/X,
| allegedly their ecosystem is production-ready".
| dwattttt wrote:
| > "just make it work one way or another, who cares if
| it's a hardcoded string, we have to ship the fix ASAP"
|
| Sure, but now that there's a "correct" way to do this,
| you don't get to complain that the hacky thing you did
| needs to keep being supported. You fix the hacky thing
| you did, or you make peace that you're still doing the
| hacky thing, problems it causes and all.
| estebarb wrote:
| That is the kind of stuff I would have expected `go vet`
| to fix.
| gwd wrote:
| The frustrating thing is that the error in question already
| _is_ a sentinel error -- Grafana (the top-level culprit in the
| linked search) should be using `errors.As(
| &http.MaxBytesError{})` rather than doing a string compare.
|
| The whole point of Hyrum's Law is that it doesn't matter how
| well you design your API: no matter what, people will depend on
| its behavior rather than its contract.
| sssddfffdssasdf wrote:
| But it looks like that until 3 years ago, this string
| comparison was the only way to do it.
| https://github.com/golang/go/pull/49359/files
| gwd wrote:
| Good catch. So in a sense this isn't really Hyrum's Law
| (which would be more appropriate to things like the Sim
| City / Windows 3.x UAF bug described in a sibling comment);
| it's more like, if people need to do something, and you
| don't give people an explicit way to do it, they'll find an
| implicit way, and then you're stuck supporting whatever
| that happened to be.
| ekidd wrote:
| There was a well-known trick in MacOS development in the
| 90s. You couldn't always avoid relying on undocumented
| behavior. The docs were incomplete and occasionally
| vague.
|
| What you _could_ do was try to rely on the _same_
| undocumented behavior as everyone else. This way, if
| Apple broke you, they 'd break half their ecosystem at
| the same time.
| lokar wrote:
| Or they could have fixed the error (adding the type)
| instead of matching the string.
| LudwigNagasena wrote:
| Early Go lacked lots of features such as errors.As. It was
| and still is sometimes idiomatic to generate Go because it is
| so featureless and writing it is often a chore. So it is very
| much about how well you design your API.
| adontz wrote:
| Honestly, this is so much worse than "catch". It's what a
| "catch" would look like in "C".
| hambes wrote:
| It might look worse than catch, but it's much more
| predictable and less goto-y.
| guappa wrote:
| goto was only bad when used to save code and jump
| indiscriminately. To handle errors is no problem at all.
| froh wrote:
| yes, yes, yes! see the Linux Kernel for plenty of such
| good and readable uses of go-to, considered useful: "on
| error, jump there in the cleanup sequence ..."
| _flux wrote:
| ..as long as you don't make mistakes. I fixed enough goto
| bugs in Xorg when I was fixing Coverity-issues in Xorg
| that I can see the downsides of this _easy_ way of error
| handling.
| int_19h wrote:
| If "catch" is goto-y (and it kinda is), then so is "defer".
| kbolino wrote:
| The biggest difference between try-catch and error values
| syntactically IMO is that the former allows you to handle a
| specific type of error from an unspecified place and the
| latter allows you to handle an unspecified type of error from
| a specific place. So the type checking is more cumbersome
| with error values whereas enclosing every individual source
| of exceptions in its own try-catch block is more cumbersome
| than error values. You usually don't do that, but you usually
| don't type-check error values either.
| cedws wrote:
| Code that checks raw error strings is just plain bad and should
| be exempt from Go's backwards compatibility guarantees. There
| is almost never an excuse for it, especially in stdlib.
| karel-3d wrote:
| Using string error comparisons was the only way to do this few
| years ago; and Go has a backwards compatibility promise.
| adontz wrote:
| This is a good example of "stringly typed" software. Golang
| designers did not want exceptions (still have them with
| panic/recover), but untyped errors are evil. On the other hand,
| how would one process typed errors without pattern matching?
| Because "catch" in most languages is a [rudimentary] pattern
| matching.
|
| https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
| KRAKRISMOTT wrote:
| Go has typed errors, it just didn't use it in this case.
| Svip wrote:
| The consumer didn't, but the error in the example is typed,
| it's called `MaxBytesError`.
| eptcyka wrote:
| Matching the underlying type when using an interface never
| feels natural and is definitely the more foreign part of
| Go's syntax to people who are not super proficient with it.
| Thus, they fall back on what they know - string comparison.
| simiones wrote:
| Only since go 1.19. It was a stringy error since go 1.0
| until then.
| TheDong wrote:
| It has typed errors, except every function that returns an
| error returns the 'error' interface, which gives you no
| information on the set of errors you might have.
|
| In other statically typed languages, you can do things like
| 'match err' and have the compiler tell you if you handled all
| the variants. In java you can `try { x } catch
| (SomeTypedException)` and have the compiler tell you if you
| missed any checked exceptions.
|
| In go, you have to read the recursive call stack of the
| entire function you called to know if a certain error type is
| returned.
|
| Can 'pgx.Connect' return an `io.EOF` error? Can it return a
| "tls: unknown certificate authority" (unexported string only
| error)?
|
| The only way to know is to recursively read every line of
| code `pgx.Connect` calls and take note of every returned
| error.
|
| In other languages, it's part of the type-signature.
|
| Go doesn't have _useful_ typed errors since idiomatically
| they're type-erased into 'error' the second they're returned
| up from any method.
| rocqua wrote:
| Exceptions in Python and C are the same. The idea with
| these is, either you know exactly what error to expect to
| handle and recover it, or you just treat it as a general
| error and retry, drop the result, propagate the error up,
| or log and abort. None of those require understanding the
| error.
|
| Should an unexpected error propagate from deep down in your
| call stack to your current call site, do you really think
| that error should be handled at this specific call-site?
| adontz wrote:
| Nope, exceptions in Python are not the same. There are a
| lot of standard exceptions
|
| https://docs.python.org/3/library/exceptions.html#concret
| e-e...
|
| and standard about exception type hierarchy
|
| https://github.com/psycopg/psycopg/blob/d38cf7798b0c602ff
| 43d...
|
| https://peps.python.org/pep-0249/#exceptions
|
| Also in most languages "catch Exception:" (or similar
| expression) is considered a bad style. People are taught
| to catch specific exceptions. Nothing like that happens
| in Go.
| vlovich123 wrote:
| C also doesn't have exceptions and C++ similarly can
| distinguish between exception types (unless you just
| throws a generic std::exception everywhere).
| rocqua wrote:
| Sure, there is a hierarchy. But the hierarchy is open.
| You still need to recurse down the entire call stack to
| figure out which exceptions might be raised.
| TheDong wrote:
| Yes, python and C also do not have properly statically
| typed errors.
|
| In python, well, python's a dynamically typed language so
| of course it doesn't have statically typed exceptions.
|
| "a better type system than C" is a really low bar.
|
| Go should be held to a higher bar than that.
| inlined wrote:
| You actually should never return a specific error pointer
| because you can eventually break nil checks. I caused a
| production outage because interfaces are tuples of type and
| pointer and the literal nil turns to [nil, nil] when
| getting passed to a comparator whereas your struct return
| value will be [nil, *Type]
| stouset wrote:
| It's really hard to reconcile behavior like this with
| people's seemingly unshakeable love for golang's error
| handling.
| adontz wrote:
| Nobody teaches people to use them. There is no analog to
| "catch most specific exceptions" culture in other languages.
| simiones wrote:
| In principle. In practice, most Go code, and even significant
| parts of the Go standard library, return arbitrary error
| strings. And error returning functions _never_ return
| anything more specific than `error` (you could count the
| exceptions in the top 20 Go codebases on your fingers, most
| likely).
|
| Returning non-specific exceptions is virtually encouraged by
| the standard library (if you return an error struct, you run
| into major issues with the ubiquitous `if err != nil` "error
| handling" logic). You have both errors.New() and fmt.Errorf()
| for returning stringly-typed errors. errors.Is and errors.As
| only work easily if you return error constants, not error
| types (they can support error types, but then you have to do
| more work to manually implement Is() and As() in your custom
| error type) - so you can't easily both have a specific error,
| but also include extra information with that error.
|
| For the example in the OP, you have to do a lot of extra work
| to return an error that can be checked without string
| comparisons, but also tells you what was the actual limit. So
| much work that this was only introduced in Go 1.19, despite
| MaxBytesReader existing since go 1.0 . Before that, it simply
| returned errors.New("http: request body too large") [0].
|
| And this is true throughout the standard library. Despite all
| of their talk about the importance of handling errors, Go's
| standard library was full of stringly-typed errors for most
| of its lifetime, and while it's getting better, it's still a
| common occurrence. And even when they were at least using
| sentinel errors, they rarely included any kind of machine-
| readable context you could use for taking a decision based on
| the error value.
|
| [0] https://cs.opensource.google/go/go/+/refs/tags/go1:src/pk
| g/n...
| kbolino wrote:
| You do not have to do more work to use errors.Is or
| errors.As. They work out of the box in most cases just
| fine. For example: package example
| var ErrValue = errors.New("stringly") type
| ErrType struct { Code int
| Message string } func (e ErrType) Error()
| string { return fmt.Sprintf("%s (%d)",
| e.Message, e.Code) }
|
| You can now use errors.Is with a target of ErrValue and
| errors.As with a target of *ErrType. No extra methods are
| needed.
|
| However, you can't compare ErrValue to another
| errors.New("stringly") by design (under the hood,
| errors.New returns a pointer, and errors.Is uses simple
| equality). If you want pure value semantics, use your own
| type instead.
|
| There _are_ Is and As interfaces that you _can_ implement,
| but you rarely _need_ to implement them. You can use the
| type system (subtyping, value vs. pointer method receivers)
| to control comparability in most cases instead. The only
| time to break out custom implementations of Is or As is
| when you want semantic equality to differ from ==, such as
| making two ErrType values match if just their Code fields
| match.
|
| The one special case that the average developer should be
| aware of is unwrapping the cause of custom errors. If you
| do your own error wrapping (which is itself rarely
| necessary, thanks to the %w specifier on fmt.Errorf), then
| you need to provide an Unwrap method (returning either an
| error or a slice of errors).
| kbolino wrote:
| Probably worth noting that errors.As uses assignability
| to match errors, while errors.Is is what uses simple
| equality. Either way, both work well without custom
| implementations in the usual cases.
| dgunay wrote:
| If you want errors to behave more like value types, you
| can also implement `Is`. For example, you could have your
| `ErrType`'s `Is` implementation return true if the other
| error `As` an `ErrType` also has the same code.
| unscaled wrote:
| Go errors cannot be string-typed, since they need to
| implement the error interface. The reason testing error types
| sometimes won't work is that the error types themselves may
| be private to the package where they are defined or that the
| error is just a generic error created by errors.New().
|
| In this case the Error has an easy-to-check public type
| (*MaxBytesError) and the documentation clearly indicates
| that. But that has not always been the case. The original sin
| is that the API returned a generic error and the only way to
| test that error was to use a string comparison.
|
| This is an important context to have when you need to make
| balanced decisions about Hyrum's law. As some commentators
| already mentioned, you should be wary of taking the extreme
| version of the law, which suggest that every single
| observable behavior of the API becomes part of the API itself
| and needs to be preserved. If you follow this extreme
| version, every error or exception message in every language
| must be left be left unchanged forever. But most client code
| doesn't just go around happily comparing exception messages
| to strings if there is another method to detect the
| exception.
| karel-3d wrote:
| They didn't have them when they implemented this code.
|
| Back then, error was a glorified string. Then it started
| having more smart errors, mostly due to a popular third party
| packages, and then the logic of those popular packages was
| more or less* put back to go.
|
| * except for stacktraces in native errors. I understand that
| they are not there for speed reasons but dang it would be
| nice to have them sometimes
| stouset wrote:
| Go didn't have them at the time.
|
| Pessimistically this is yet another example of the language's
| authors relearning why other languages have the features they
| do one problem at a time.
| apitman wrote:
| When I clicked on the link to codebases relying on the specific
| error string, I was expecting to see random side projects. Wasn't
| expecting to see Grafana and Caddy on the list.
| Cthulhu_ wrote:
| Never underestimate the mediocrity of known large codebases,
| lol.
|
| (just kidding, they're not mediocre, but they're not infallible
| or perfect either)
| apitman wrote:
| This instance doesn't necessarily indicate they did anything
| wrong. See sibling.
| gwd wrote:
| To be fair to those projects, the type was introduced only
| three years ago:
|
| https://github.com/golang/go/pull/49359/files
|
| Before that, doing a string compare was basically the only way
| to detect that specific error. That was definitely an omission
| on the part of the original authors of the stdlib code; I don't
| it should be classified as "Hyrum's Law".
| apitman wrote:
| Yeah I don't doubt it was the best option. Just a bit
| surprised.
| mholt wrote:
| Hey Anders; Francis notified us of this today. I didn't realize
| a proper type had been created. We'll update our code.
| turtleyacht wrote:
| In Docker's error response for `docker rmi'; the fifteenth word
| is "container" and the sixteenth is the container ID.
| fullstackchris wrote:
| Sure... but this is why we have sem versioning and release notes.
| It's always nice to try and support all users but sometimes you
| just need to ship breaking changes...
| Cthulhu_ wrote:
| While in principle you're correct, Go the language is very
| dedicated to backwards and forwards compatibility; while
| there's been talk of a Go 2 for a long time now, they're not
| eager to go there and if they do, they intend to make the
| transition low impact.
|
| That said, I'd say this is an excellent candidate to deprecate
| or warn about now, and to make impossible in a version 2. Then
| again, how would you even stop this? A string representation of
| an error is common in any language, you need it to log things.
|
| I think at best there will be a static analysis rule (in e.g.
| go vet) that tries to figure out if any matching is done on the
| string representation of an error.
| TheDong wrote:
| > I think at best there will be a static analysis rule (in
| e.g. go vet) that tries to figure out if any matching is done
| on the string representation of an error.
|
| First they'd need to export the errors the stdlib returns
| https://news.ycombinator.com/item?id=41507714
|
| I wouldn't hold my breath on that one.
| fullstackchris wrote:
| I'm not talking about Go itself, I'm talking about building
| an API. All this talk of "string vs type" is not the solution
| to the root problem - sure, types can be better to return but
| what if the type changes? You still have breaking changes.
| hifromwork wrote:
| Hyrum's law is specifically about how every change is a
| breaking change if you have enough users. So it's always a bit
| subjective. No sane person considers changing an error message
| a breaking change in context of semver. It's just go going
| above and beyond to take care of backward compatibility.
| raverbashing wrote:
| It's like an inverted game of cat and mice
|
| 1 - Lang/OS/Lib developer puts out a quirky or buggy API (or even
| just an ok API)
|
| 2 - Developers rely on a quirky, weird or unexpected side effect
| because it's easier/more obvious or it just works this way due to
| a bug
|
| 3 - Original developer can't fix it because it would break
| compatibility
|
| 4 GOTO 1
| withinboredom wrote:
| Immediately reminded of this:
| https://externals.io/message/126011 that is an ongoing
| conversation in php-internals about removing a quirky/buggy
| behavior from PHP that, at the very end (at least of this
| comment's time) someone jumps in and says "yep, its useful,
| please keep it"
| Cthulhu_ wrote:
| And this isn't even quirky/buggy, it's just the string
| representation of an error. That said, Go took a while to
| improve its core error mechanisms and add utilities for
| matching errors by type instead of its string representation.
| simiones wrote:
| In this case, it really is - because until Go 1.19, that
| function simply returned `errors.New("http: request body too
| large")`. So until Go 1.19, there really was no other way to
| check if this error occurred than `err.String() == "http:
| request body too large"`. Even if we had had errors.Is/As
| earlier, it wouldn't have helped in this case.
| FiloSottile wrote:
| Hah, I wrote the crypto/rsa comments. We take Hyrum's Law (and
| backwards compatibility [1]) extremely seriously in Go. Here are
| a couple more examples:
|
| - We randomly read an extra byte from random streams in various
| GenerateKey functions (which are not marked like the ones in OP)
| with MaybeReadByte [2] to avoid having our algorithm locked in
|
| - Just yesterday someone reported that a private ECDSA key with a
| nil public key used to work, and now it doesn't, so we probably
| have to make it work again [3]
|
| - Iterating over a map uses a randomized order to avoid exposing
| the internals
|
| - The output of rand.Rand is considered part of the compatibility
| promise, so we had to go to great lengths to improve it [4]
|
| - We discuss _all the time_ what commitments to make in docs and
| what behaviors to disclaim, knowing we can never change something
| documented _and_ probably something that 's not explicitly
| documented as "this may change" [6]
|
| [1]: https://go.dev/doc/go1compat
|
| [2]: https://pkg.go.dev/crypto/internal/randutil#MaybeReadByte
|
| [3]: https://go.dev/issue/70468
|
| [4]: https://go.dev/blog/randv2
|
| [5]: https://go.dev/blog/chacha8rand
|
| [6]: https://go-
| review.googlesource.com/c/go/+/598336/comment/5d6...
| mkesper wrote:
| The nil key case really makes me wonder how sane it is to
| support these cases. You will be forced to lug this broken
| behavior with you forever, like the infamous A20 line
| (https://en.wikipedia.org/wiki/A20_line).
| FiloSottile wrote:
| > You will be forced to lug this broken behavior with you
| forever
|
| Yep, welcome to my life.
| atsjie wrote:
| Wouldn't that broken behaviour be a potential security
| issue by itself?
|
| I do remember Go making backwards incompatible changes in
| some rare scenarios like that.
|
| (and technically the loopvar fix was a big backwards
| incompatible change; granted that was done with a lot of
| consideration)
| whizzter wrote:
| Wouldn't a nil ECDSA key be a security risk?
| unscaled wrote:
| If a private key is available, the public key can be
| derived from the private key using scalar multiplication.
| This is how ecdsa.GenerateKey works by itself - it first
| generates a private key from the provided random byte
| stream and then derives a public key from that private
| key.
|
| I don't see how this can be a security risk, but allowing
| a public key that has a curve but a nil value is
| definitely a messy API.
| gnfargbl wrote:
| As a user of your code this is true, and I'm very grateful
| indeed that you take this approach.
|
| I would add as a slight caveat that to benefit from this
| policy, users absolutely must read the release notes on major
| go versions before upgrading. We recently didn't, and we were
| burnt somewhat by the change to disallow negative serial
| numbers in the x509 parser without enabling the new feature
| flag. Completely our fault and not yours, but I add the caveat
| nevertheless.
| FiloSottile wrote:
| We have gotten a liiiiittle more liberal ever since we
| introduced the new GODEBUG feature flag mechanism.
|
| I've been meaning to write a "how to safely update Go" post
| for a while, because the GODEBUG mechanism is very powerful
| but not well-known and we could build a bit of tooling around
| it.
|
| In short, you can upgrade your toolchain without changing the
| go.mod version, and these things will keep working like they
| did, and set a metric every time the behavior _would_ have
| changed, but didn 't. (Here's where we could build a bit of
| tooling to check that metric in prod/tests/CLIs more easily.)
| Then you can update the go.mod version, which updates the
| default set of GODEBUGs, and if anything breaks, try
| reverting GODEBUGs one by one.
| gnfargbl wrote:
| That sounds good.
|
| Breaking changes in major version updates is a completely
| normal thing in most software and we usually check for it.
| Ironically the only reason we weren't previously bothering
| in go is that the maintainers were historically so hyper-
| focused on absolute backwards compatibility that there were
| never any breaking changes!
| boloust wrote:
| Ironically, I once wrote a load balancer in Go that relied on
| the randomized map iteration ordering.
| aleksi wrote:
| That is a bad idea: https://dev.to/wallyqs/gos-map-iteration-
| order-is-not-that-r...
| OskarS wrote:
| Man, you really can't escape Hyrum's Law ever! Now we have
| people depending on the iteration order being random!
| ahoka wrote:
| That's why it's totally stupid to randomize it.
| dwattttt wrote:
| Clearly you need to randomly decide whether or not to
| randomise it.
| unscaled wrote:
| > We randomly read an extra byte from random streams in various
| GenerateKey functions (which are not marked like the ones in
| OP) with MaybeReadByte [2] to avoid having our algorithm locked
| in
|
| You don't seem to do that in ed25519. Back before
| ed25519.NewKeyFromSeed() existed, that was the only way to
| derive a public Ed25519 key from a private key, and I'm pretty
| sure I've written code that relied on that (it's easy to
| remember, since I wasn't very happy about it, but this was all
| I could do). The documentation of ed25519.GenerateKey mentions
| that the output is deterministic, so kudos for that. It seems
| you've really done a great job with investigating and
| maintaining ossified behavior in the Go cryptography APIs and
| preventing new ones from happening.
| mjw_byrne wrote:
| The map iteration order change helps to avoid breaking changes
| in future, by preventing reliance on any specific ordering, but
| when the change was made it was breaking for anything that was
| relying on the previous ordering behaviour.
|
| IMO this is a worthwhile tradeoff. I use Go a lot and love the
| strong backwards compatibility, but I would happily accept a
| (slightly) higher rate of breaking changes if it meant greater
| freedom for the Go devs to improve performance, add features
| etc.
|
| Based on the kind of hell users of other ecosystems seem
| willing to tolerate ( _cough_ Python _cough_ ), I believe I am
| not alone in this viewpoint.
| wild_egg wrote:
| Data point of one, but I've been using Go since 2012 and
| would drop it instantly if any of the backwards compatibility
| guarantees were relaxed.
|
| Having bugs imposed on you from outside your project is a
| waste of time to deal with and there are dozens of other
| languages you can pick from if you enjoy that time sink. Most
| of them give you greater capabilities as the balance.
|
| Go's stability is a core feature and compensates for the lack
| of other niceties. Adding features isn't a good reason to
| break things. I can go use something else if I want to make
| that trade.
| otterley wrote:
| Respectfully, I don't think you would just pack up and
| leave. The cost of switching to an entirely different
| language--which might have even worse backwards
| compatibility issues--is significantly higher than fixing
| bugs you inadvertently introduced due to prior invalid
| assumptions.
|
| I'd call your bluff.
| hn34381 wrote:
| Also, there is a time and a place for things.
|
| Breaking API changes in a minor version update sucks and
| is often an unexpected time sink, and often mandatory
| because it has some security patch, critical bug fix, or
| something.
|
| Breaking API changes in a major version update is
| expected, can be planned for, and often can be delayed if
| one chooses.
| jerf wrote:
| Breaking iteration order was also well established as a valid
| move. Several other languages had already made a similar
| change, much later in their own lifecycle than Go did. That
| helps a lot, because it shows it is largely just an
| annoyance, mostly affecting tests.
| ljm wrote:
| I'd consider stuff like that part of the opinion the language
| has. Go's opinion is that backwards compatibility at all
| reasonable cost is a priority.
|
| When it comes to ecosystems, the opinions have trade-offs. I
| would say that Go's approach to dependencies, modules and
| workspaces is one of those. As a language it mostly stays out
| of your way, but correcting imports because it pulled in the
| wrong version, or dealing with go.mod, go.work and replace
| directives in a monorepo, gets old pretty fast (to the extent
| it's easier to just have a monorepo-wide go.mod with
| literally every dependency in it). At least it's an
| improvement over having to use a fairly specific directory
| structure though.
| hinkley wrote:
| Java 5 was a fun upgrade for a lot of people because it
| caused JUnit tests to run in a different order. Due to
| hashtable changes altering the iteration of the reflected
| function names.
|
| Don't couple your tests, kids!
| abtinf wrote:
| This is one of the least appreciated aspects of Go. Code I
| wrote 12 years ago still _just works_.
| indulona wrote:
| This is why we have semantic versioning.
| simiones wrote:
| Semantic versioning does nothing to help here. If you don't
| realize that people are depending on such a behavior, you won't
| increment the major version number.
| mseepgood wrote:
| And if you realize it (as in this case) you probably don't
| want to increase the major version number either, but leave
| it as-is (unless you follow the CADT model of
| maintainership).
| cloverich wrote:
| I think a very nice middle ground is when you decide to
| remove something, to mark it as deprecated in the next
| major version, and remove it in the one after. Not always
| possible, but IIRC React does this; so I'd frequently
| upgrade, then start seeing deprecation warning messages (in
| dev); I'd then have a clear signal before upgrading to the
| next version. It helped that major versions did not arrive
| often so making this kind of change was only occasionally
| necessary.
|
| A bit trickier in this case no doubt; and trade offs. Ive
| not minded the React updates over the years, but busting
| out the Go code I wrote many years ago and having it still
| run flawlessly is amazing too.
| voiper1 wrote:
| Related XKCD: https://xkcd.com/1172/
| red_admiral wrote:
| Should this not be handled by checking "resp.status == 413" ?
| lovasoa wrote:
| An interesting topic is how to fight Hyrum's law. A possibility
| is to add randomness in things you don't want people to rely on.
| If I remember well, this is what the QUIC protocol does. Some
| fields are unused in the current version, but required by the
| specification to be set to random values, not null bytes, so that
| routers don't start relying on them to identify the packets.
|
| EDIT.
|
| I think I found the source: https://www.rfc-
| editor.org/rfc/rfc9000#section-17.2.1
|
| > The value in the Unused field is set to an arbitrary value by
| the server. Clients MUST ignore the value of this field. [...]
| Note that other versions of QUIC might not make a similar
| recommendation.
|
| I think they call it "greasing", to prevent "ossification".
| rho4 wrote:
| Interesting thanks! Might indeed be valuable to add to one's
| toolbox.
| vitus wrote:
| > I think they call it "greasing"
|
| This is a reference to RFC 8701, which coined the acronym
| GREASE ("Generate Random Extensions And Sustain
| Extensibility"), first in the context of TLS.
|
| https://www.rfc-editor.org/rfc/rfc8701.html
|
| (The earliest draft of the RFC dates back to mid-2016, which is
| likely the first public mention of the term:
| https://datatracker.ietf.org/doc/html/draft-davidben-tls-
| gre...)
| klabb3 wrote:
| This is wonderful. I'm quite familiar with QUIC but hadn't
| heard about this.
|
| Nothing like waking up after 10 years, realize you now really
| need those bits, and 20 different routers from 10 brands have
| decided that those bits must be a certain way.
|
| Bonus points for checksums/crypto that breaks on the other end
| if the bits have been messed with. Curse those middle-boxes and
| their "clever hacks".
| sixfiveotwo wrote:
| Quite interesting, thank you.
|
| However, in this specific instance, even if the text cannot be
| changed, couldn't the error itself in the server be processed and
| signaled differently, eg. by returning a Status Code 413[1],
| since clients ought to recognize that status code anyway?
|
| [1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413
| majewsky wrote:
| Since the caller gets this as an error object, instead of as a
| plain string, it seems likely that this is within the same
| process, i.e. a library function returns the MaxBytesError to a
| level higher in the business logic, without a network
| transmission inbetween.
| djoldman wrote:
| One interesting metric for LLMs is that for some tasks their
| precision is garbage but recall is high. (in essence: their top 5
| answers are wrong but top 100 have the right answer).
|
| As relates to infinite context, if one pairs the above with some
| kind of intelligent "solution-checker," it's interesting if
| models may be able to provide value across absolute monstrous
| text sizes where it's critical to tie two facts that are worlds
| apart.
| mormegil wrote:
| This probably didn't belong here?
| djoldman wrote:
| It didn't! Thanks
| littlestymaar wrote:
| > so per Hyrum's Law it's _probably_ relied upon by some.
|
| Yikes. this kind of defensive posture with respect to Hyrum's law
| is extreme and absurd. Per Hyrum's Law _everything_ is
| potentially relied upon by someone, keeping stuff that _may be_
| relied upon means you cannot change anything (see this infamous
| xkcd on this[1])!
|
| Thinking that no change is acceptable at all isn't the right
| take-away from Hyrum's Law: instead you should _be ready_ to have
| to roll back changes that break people 's workflow even when you
| didn't expected the change to break anything (and it also means
| that you need to have a way for your users to communicate their
| issues to you, which definitely isn't something Google is well-
| known for ...).
|
| [1]: https://xkcd.com/1172/
| jen20 wrote:
| The Go project is in fact VERY well known for accepting user
| feedback, via golang-nuts or GitHub issues.
| gr4vityWall wrote:
| This line of the article resonates with me a lot: > A good
| reminder to be careful when changing code others might depend on
|
| If you maintain a widely use Free Software library, please
| consider avoiding breaking changes when possible.
|
| I'm not going to imply you have any obligation to do so, but your
| users will appreciate it.
| kibwen wrote:
| Hyrum's Law is one of those observations that's certainly useful,
| but be careful not to fixate on it and draw the wrong
| conclusions. Consider that even the total runtime of a function
| is an observable property, which means that optimizing a function
| to make it faster is a breaking change (what if suddenly one of
| your queues clears too fast and triggers a deadlock??), despite
| the fact that 99.99999999% of your users would probably
| appreciate having code that runs faster for no effort on their
| part.
|
| Therefore it's unavoidable that what constitutes a "breaking
| change" is a social contract, not a technical contract, because
| the alternative is that literally nothing is ever allowed to
| change. So as a library author, document what parts of your API
| are guaranteed not to change, be reasonable, and have empathy for
| your users. And as a library consumer, understand that making
| undocumented interfaces into load-bearing constructs is done at
| your own risk, and have empathy for the library authors.
| rjst01 wrote:
| One day I will give a lighting talk about the load bearing
| teapot, or how and why I made HTTP Status 418 a load bearing
| part of an internal API, and why it was the least bad option
| considering the constraints.
| renewiltord wrote:
| It's a classic. Binance will give you 429 errors to back off
| then 418s to tell you you will be IP banned and then they'll
| ban you.
| hinkley wrote:
| Google's spiders will punish you for giving them too many
| 429 responses. It's hell for hosting sites with vanity
| urls. They can't tell they're sending you 50+ req/s.
|
| It's practically a protection racket. Only AWS gets the
| money.
| wbl wrote:
| 50 requests/sec? Did you forget a few zeros?
| hinkley wrote:
| Little's law is a bitch, and you can get away with a
| little throttling but not much.
|
| Also, that's a bit dismissive for HN.
| skovati wrote:
| reminds me of: https://xkcd.com/1172/
| citizenpaul wrote:
| That one always fell flat for me, but I get it. The idea that
| an emacs user would communicate with another human rather
| than tinker with their config to deal with the change is
| unrealistic. /s /sorta
| materielle wrote:
| I think everything you said is totally correct for open source
| library owners.
|
| But let me offer a different perspective: Hyrum's law is
| neither a technical contract nor a social contract. It's an
| emergent technical property in a sufficiently used system.
|
| How you respond to that emergent property depends on the social
| context.
|
| If you are a FOSS maintainer, and an optimization speeds up
| 99.99% of users and requires 0.01% to either fix their code or
| upgrade to a new API, you ship it.
|
| If you are working at a big tech company, you need _both_ the
| optimization and breaking 0% of the company. So you will work
| across teams to find the sweet spot.
|
| If you are an enterprise software company, and you change
| breaks 0.1% if users, but that user is one of the top 5
| contracts, you don't ship.
| uoaei wrote:
| > It's an emergent ... property in a sufficiently used system
|
| This is also a sufficient description of "social contract"
| for this context.
| kmacdough wrote:
| Seems like you're saying the same thing, just using "social
| contract" differently. I think they use social contract not
| to mean binding, but to highlight the fact that Hyrums Law
| must be taken in the social context of the project. In the
| case of large SW company, the social contract would be to not
| break services, even when folks are misusing an API. And for
| a popular open source project, it would mean not breaking a
| widely used behavior, even if it isn't specified or
| officially supported. Determining the social contract seems
| to be precisely what you describe as "not a social contract".
| bluGill wrote:
| That used to be a problem in the 1980s. Thus PCs came with a
| turbo button to slow them down, and 8 bit computers went the
| entire decade without upgrading their speed even though faster
| CPUs were available. These days nearly everything runs on more
| than one CPU and so nobody relies on function runtime (other
| than is if fast enough). Even in embedded they have been burned
| by their one CPU going out of production and so try to avoid
| that dependency because it cannot be relied on anymore.
| hinkley wrote:
| I found a used copy of Warcraft III and found it was
| unplayable because the scrolling algorithm ran as fast as
| possible with no minimum time. Any map bigger than 2x2
| screens you could not scroll to the middle.
| ljm wrote:
| I feel like this is approaching absurdity, if only because
| something like the total runtime of a function is not under the
| control of the author of the function. The operating
| environment will have an impact on that, for example, as will
| the load the system is currently experiencing. A GC pass can
| affect it.
|
| In short, I wouldn't consider emergent behaviours of a machine
| as part of an intentional interface or any kind of contract and
| therefore I wouldn't see it as a breaking change, the same as
| fixing a subtle bug in a function wouldn't be seen as a
| breaking change even if someone depended on the unintentional
| behaviour.
|
| I think it's more of a testament to Go's hardcore commitment to
| backwards compatibility, in this case, than anything else.
| skybrian wrote:
| Yes, it's an absurd example to make a point. We don't
| normally consider performance in scope for what's considered
| a breaking API change and there are good reasons for that,
| including being non-portable. Performance guarantees are what
| hard real-time systems do and they're hardware-specific.
| (There is also the "gas fee" system that Ethereum has, to
| make a performance limit consistent across architectures.)
|
| But there are still informal limits. If the performance
| impact is bad enough, (say, 5x slower, or changing a linear
| algorithm to quadratic), it's probably going to be reverted
| anyway. We just don't have any practical way of formalizing
| rough performance guarantees at an API boundary.
| kibwen wrote:
| _> If the performance impact is bad enough_
|
| Even worse, it's possible to select a new algorithm that
| improves the best-case and average-case runtimes while
| degrading the worst-case runtime, so no matter what you do
| it will punish some users and reward others.
| AlotOfReading wrote:
| It's quite common in cryptography for the runtime to be
| important. For example, password verification time shouldn't
| depend on the value of the key or the password. Systems have
| been broken because someone wrote a string compare that
| returned early.
| ljm wrote:
| And, since most languages short circuit on basic string
| comparisons, you'd have some form of `secure_compare`
| function that compares two strings in constant time, and
| that behaviour is contracted in the name of the function.
|
| Nobody is rewriting `==` to compare strings in constant
| time, not because it breaks some kind of API contract, but
| because it would result in a massive waste of CPU time. The
| point is, though, that they could. But then they are
| deciding to sacrifice performance for this one problem.
|
| Crypto is obviously a case of it own when it comes to
| optimisations and as much as I called out the parent for
| approaching the absurd, we can pull out many similar
| special cases of our own.
| SatvikBeri wrote:
| I once sped up a very suboptimal routine that from ~100s to
| ~.1s (it was doing database lookups in a for loop), and that
| broke the reporting system because the original author had made
| several asynchronous function calls and assumed they would have
| all finished running by the time the (formerly) slow routine
| was done. Figuring out exactly what happened took forever.
| tshaddox wrote:
| > Consider that even the total runtime of a function is an
| observable property, which means that optimizing a function to
| make it faster is a breaking change
|
| Well yeah, that's pretty much the textbook example of Hyrum's
| Law (or some funnier variation like "I was relying on the heat
| from the CPU to warm my bedroom, can you please revert your
| change that improved CPU performance").
| brundolf wrote:
| Reminds me of: https://xkcd.com/1172/
| wodenokoto wrote:
| At work we have a data providers whose API always returns 200,
| often with the text content "ERROR: ..."
|
| And that's how put Hyrums law into effect.
| mox111 wrote:
| Perhaps some package authors are more accepting of this than
| others. I stumbled upon this comment in the `json` package the
| other day:
|
| // isValidNumber reports whether s is a valid JSON number
| literal. // // isValidNumber should be an internal detail, // but
| widely used packages access it using linkname. // Notable members
| of the hall of shame include: // - github.com/bytedance/sonic
| algorithmsRcool wrote:
| Another related effect of this is Protocol Ossification [0] which
| happens when implementers of a public API/Protocol surface area
| take implicit dependencies on common but not standardized
| behaviors of the API/Protocol implementation.
|
| That being said, you can take proactive steps to defeat this. For
| example, the default Hash for strings in .NET is randomly seeded
| each time a process starts[1] in order to strongly dissuade folks
| from taking an implicit dependency on the underlying algorithm
| which is not guaranteed to be stable
|
| [0] : https://en.wikipedia.org/wiki/Protocol_ossification
|
| [1] : https://andrewlock.net/why-is-string-gethashcode-
| different-e...
| dhosek wrote:
| At one job, I found a misspelling in an error message and fixing
| it only to discover that the web of dependencies on that
| misspelled text was so deep that it was impractical to fix and
| had to return to the misspelled text. It still bugs me.
| forbiddenlake wrote:
| Are you Phillip Hallam-Baker? :)
|
| https://en.wikipedia.org/wiki/HTTP_referer
| fulafel wrote:
| At least in the post context there's still time to fix
| "Golang".
| lokar wrote:
| People who parse text error messages deserve what they get
| jgeada wrote:
| I think Hyrum's law really depends on APIs not applying
| consequences to people that depend on non-guaranteed behavior.
| The world needs more consequences for poor behavior.
|
| Just randomly change the non-guaranteed stuff in every release
| and this behavior likely would stop and/or you'd lose the users
| that don't know any better. Both sides of that sound like a win
| to me.
| inlined wrote:
| Go seems really sensitive to this subject. Maps iterate in order,
| but one day they said "this is incidental and we said not to rely
| on it. You do, so we're breaking it in a minor release" and now
| maps iterate in order... from a random offset
| kbolino wrote:
| On the one hand, I never realized that map iteration order was
| consistent, but it's just the starting point that changes. On
| the other hand, I guess there's no other way to do it, since a
| proper shuffle would require O(n) bookkeeping. I suppose you
| could also flip a coin for going backwards too.
| souenzzo wrote:
| Clojure manages to do improvement to the language, without
| breaking the users.
|
| Most of improvements are "additions", it is never a "change" or
| "re-do something better"
|
| It is an awesome experience be able to upgrade the language
| anytime with no fear or pain.
| ivanjermakov wrote:
| I don't know. I think Hyrum's law should not prevent the project
| from advancing. If the user is so dependent on non-contact
| behavior of the API, they have to expect that some logic might
| break even on minor version updates.
| jnordwick wrote:
| Does anybody else always read this as Hyrule's Law?
| aidenn0 wrote:
| Many occurrences of Hyrum's are "desire paths[1]" of APIs. For
| many people, the most obvious way to determine if the error was a
| MaxBytesError was to check the string. I'm not familiar with Go,
| but assuming it has RTTI, I'm guessing the intended path for
| people to take was to check the type of the error against
| MaxBytesError, and occurrences of this were people who either
| didn't know that, or found the error string to be immediately
| available in their tooling, but the type not immediately
| available.
|
| [edit]
|
| Per [2], this looks desire paths is even more an apt analogy than
| I thought; until 3 years ago, this code returned a generic Error
| type.
|
| 1: https://en.wikipedia.org/wiki/Desire_path
|
| 2: https://news.ycombinator.com/item?id=42202472
| adrianmsmith wrote:
| It's interesting that this law is the exact opposite of the
| Robustness Principle / Postel's Law.
|
| > be conservative in what you send, be liberal in what you accept
|
| If you are liberal in what you accept, you'd better understand
| the ways in which you've been liberal, and document them (at
| least) internally, because you're going to have to support all
| those ways forever, even after huge codebase changes, due to
| Hyrum's Law.
|
| I try to avoid creating APIs which are "liberal in what they
| accept" for exactly that reason.
| remus wrote:
| > I try to avoid creating APIs which are "liberal in what they
| accept" for exactly that reason.
|
| That's my preference too. When you have relaxed criteria about
| what kind of data you accept via an API I find you inevitably
| end up having to make decisions about how to massage that data
| in to some sort of canonical format, and those decisions almost
| always seem to end up leading to behaviour that's surprising to
| users in one way or another.
| jameson wrote:
| what I learned from shipping APIs:
|
| 1. Clients will do whatever they need to do get their job done,
| even if it's not the publisher's intended way
|
| 2. Clients don't read documentation
|
| 3. Bugs will become part of API once enough clients rely on their
| behavior
|
| 4. The number of API calls does not necessarily equate to
| importance.
|
| ---
|
| As such, I aim for the following when developing an API
|
| 1. Ship the beta API early and see how they use it to minimize
| the surprise. (This may not always be possible)
|
| 2. In most cases, bump up the major version while supporting the
| previous version. This means you'll need to define SLA for your
| API
|
| 3. Most clients are OK with breakages as long as they are given
| enough time to migrate, or the API provider gives them a tool to
| auto-migrate the code (if that's possible in your product)
| evanfarrar wrote:
| What do they call the law that says they can't increase the major
| version?
| remus wrote:
| "Guido's Law" perhaps?
| sublimefire wrote:
| There are cases when you need to make a choice if you want to fix
| the bug as it might break many people who rely on it. There is no
| real good answer but to be able to look forward and anticipate
| the misuse.
| nicocesar wrote:
| see also https://news.ycombinator.com/item?id=16286764
| tonymet wrote:
| > to design systems in a way that minimizes the chances of
| unintended
|
| go team did their best to define a custom MaxSizeError to
| discourage developers from the flimsy string dependency.
|
| Every system hits a limit on the amount of guard rails and
| protections needed to protect foolish customers from their own
| bad behavior.
|
| Sometimes you need to deliberately break dependencies that were
| never meant to exist to reveal the vulnerabilities in a system.
| jimjimjim wrote:
| I wish someone in the Chrome team knew about Hyrum's Law before
| they released a breaking change in the way chrome validates
| custom URLs.
| zaptheimpaler wrote:
| It's sort of Hyrum's Law but it's really just Go being Go. The
| error could've been an enum type that could be changed with only
| a string replace for consumers. Instead they are using strings as
| types, so now you have no idea how consumers might rely on it.
| They could check the middle 6 chars of the error and break if you
| change it. It's another terrible anachronistic design decision
| when better alternatives have been in use in other languages for
| decades. Early mistakes + inability to change things means you're
| stuck forever.
| andrewfromx wrote:
| like https://en.wikipedia.org/wiki/HTTP_referer can't be referrer
| ever
___________________________________________________________________
(page generated 2024-11-21 23:00 UTC)